Backend Architecture
In this part, we discuss the backend design of this project. Before moving forward for the context visit this blog: DYNAMIC TABS USING CUSTOM METADATA AND LWC (Part 1)
An Apex controller class refers to a server-side Apex class i.e. used to interact with the Salesforce backend and provide data or perform operations that cannot be done directly within the Lightning Web Component itself.
CaseController
In this apex class, we are populating drop-down options from the custom metadata for the given business unit and wrapping queues, and tabs with related cases for the selected option.
In the findCases() method we are attempting to search cases by searching keywords.
public with sharing class CaseController {
@AuraEnabled(cacheable='true')
public static FICCaseWrapper getDropDownOptions(String SBU, Integer pageSize) {
List<String> optionList = CaseProcessor.getOptionList(CaseCustomMetadata.getDropDownOption(SBU));
return CaseController.getWrapCases(SBU, optionList.get(0), 0, pageSize);
}
@AuraEnabled(cacheable='true')
public static FICCaseWrapper getWrapCases(String SBU, String selectedOption, Integer offsetValue, Integer pageSize) {
Avenoir_Entity_Tab_Setting__mdt customData = CaseCustomMetadata.getQueuesForSelectedOption(selectedOption, SBU);
List<String> queueList = customData.Queues__c.split(',');
List<String> tabList = customData.Tabs__c.split(',');
return new FICCaseWrapper(
CaseProcessor.getWrapCasesWithQueue(queueList, tabList, pageSize, UserInfo.getUserId()),
CaseProcessor.getOptionList(CaseCustomMetadata.getDropDownOption(SBU))
);
}
@AuraEnabled(cacheable='true')
public static CaseWrapper getWrapCasesForSelectedQueue(String queueName, String tabName, Integer offsetValue, Integer pageSize) {
return CaseProcessor.getCasesForSelectedQueue(queueName, tabName, offsetValue, pageSize, UserInfo.getUserId());
}
@AuraEnabled
public static void updateMyCases(String caseId) {
CaseUtility.updateMyCaseOwner(caseId);
}
@AuraEnabled
public static List<Case> findCases(String searchKeyWord) {
if (Case.SObjectType.getDescribe().isAccessible()) {
return new CasesSelector().selectCasesBySearchKeyWord(searchKeyWord);
} else {
return null;
}
}
}
CaseProcessor
The class is designed to provide methods for retrieving and processing case data based on various criteria and then wrapping the results in the CaseWrapper class. Let us discuss some of these methods !!!
● getWrapCasesWithQueue: This method takes a list of queue names, a list of tab names, a page size, and a user ID as parameters. It returns a list of CaseWrapper objects. It checks if the Case object's metadata is accessible. If so, it processes cases based on queue names and tab names, either by fetching queue-specific cases or the user's cases.
● getWrappedQueueCases: This method takes a list of queue names, a list of tab names, and page size as parameters. It retrieves cases for each queue name, wraps them in CaseWrapper objects, and returns a list of these objects.
● getMyCases: This method takes a user ID and a page size as parameters. It retrieves cases associated with the user, wraps them in a CaseWrapper object, and returns it.
● getCasesForSelectedQueue: This method takes a queue name, tab name, offset value, page size, and a user ID as parameters. It retrieves cases based on the selected queue, wraps them in a CaseWrapper object, and returns it.
public with sharing class CaseProcessor {
public static List<CaseWrapper> getWrapCasesWithQueue(
List<String> queueList,
List<String> tabList,
Integer pageSize,
IduserId
) {
List<CaseWrapper> processedCases = new List<CaseWrapper>();
if (Case.SObjectType.getDescribe().isAccessible()) {
if (queueList.get(0) != 'My Cases') {
processedCases =CaseProcessor.getWrappedQueueCases(queueList, tabList, pageSize);
} else {
processedCases.add(CaseProcessor.getMyCases(userID, pageSize));
}
}
return processedCases;
}
public static List<CaseWrapper> getWrappedQueueCases(List<String> queueList, List<String> tabList, Integer pageSize) {
List<CaseWrapper> processedCases = new List<CaseWrapper>();
List<AggregateResult> totalCasesWithQueue = new CasesSelector().getTotalQueueCases(queueList);
Map<String, Integer> queueWithTotalCases = CaseProcessor.getTotalCasesWithQueue(totalCasesWithQueue);
if (queueList.size() != 0) {
for (Integer i=0; i< queueList.size(); i++) {
processedCases.add(
new CaseWrapper(
queueList.get(i),
tabList.get(i),
new CasesSelector().selectCasesByQueueNameWithOffset(queueList.get(i), 0, pageSize),
queueWithTotalCases.containsKey(queueList.get(i)) ? queueWithTotalCases.get(queueList.get(i)) : 0
)
);
}
}
return processedCases;
}
public static CaseWrapper getMyCases(String userID, Integer pageSize) {
Integer size = (new CasesSelector().getTotalCasesByUserId(userId)).size() == 0 ? 0
: Integer.valueOf((new CasesSelector().getTotalCasesByUserId(userId))[0].get('totalCases'));
return new CaseWrapper(
'My Cases',
'My Cases',
new CasesSelector().getCasesByUserId(userId, 0, pageSize),
size
);
}
public static CaseWrapper getCasesForSelectedQueue(String queueName, String tabName, Integer offsetValue, Integer pageSize, Id userId) {
CasesSelector selector = new CasesSelector();
return new CaseWrapper(
queueName,
tabName,
queueName == 'My Cases'
? selector.getCasesByUserId(userId, offsetValue, pageSize)
: selector.selectCasesByQueueNameWithOffset(queueName, offsetValue, pageSize)
);
}
public static List<String> getOptionList(List<Avenoir_Entity_Tab_Setting__mdt> options) {
List<String> optionList = new List<String>();
for (Integer i=0; i < options.size(); i++) {
optionList.add(options.get(i).Drop_Down_Option__c);
}
return optionList;
}
public static Map<String, Integer> getTotalCasesWithQueue(List<AggregateResult> casesWithQueue) {
Map<string, Integer> totalCasesWithQueue = new Map<String, Integer>();
for (Integer i=0; i < casesWithQueue.size(); i++) {
totalCasesWithQueue.put(String.valueOf(casesWithQueue[i].get('Name')), Integer.valueOf(casesWithQueue[i].get('totalCases')));
}
return totalCasesWithQueue;
}
}
CasesSelector
This class encapsulates methods to query and retrieve cases based on different parameters such as queue name, user ID, and search keywords. It supports pagination for fetching results in chunks.
public with sharing class CasesSelector {
/**
* @description
*
* @param queuename (string): queuename
* @param offsetValue (Integer): offsetValue
* @return Return Case: Return description
*/
public List<Case> selectCasesByQueueNameWithOffset(String queue, Integer offsetValue, Integer pageSize) {
return [
SELECT
Owner_Queue_Name__c,
CaseNumber,
CreatedDate,
Status,
Account.Name,
Priority,
Origin,
LastModifiedBy.Name
FROM
Case
WHERE Owner_Queue_Name__c =: queue
ORDER BY
CreatedDate
LIMIT :pageSize
OFFSET :offsetValue
];
}
public List<AggregateResult> getTotalQueueCases(List<String> queueList) {
return [
SELECT
Count(ID)totalCases,
Owner.Name
FROM
Case
Group By Owner.Name
];
}
public List<AggregateResult> getTotalCasesByUserId(Id userID) {
return [
SELECT
Count(ID)totalCases,
Owner.Name
FROM
Case
WHERE
OwnerId =:userID
Group By Owner.Name
];
}
/**
* @description
*
* @param userId (string): userId
* @param offsetValue (Integer): offsetValue
* @return Return Case: Return description
*/
public List<Case> getCasesByUserId(Id userId, Integer offsetValue, Integer pageSize) {
return [
SELECT
Owner_Queue_Name__c,
CaseNumber,
Subject,
Status,
Priority,
Origin,
CreatedDate
FROM
Case
WHERE
OwnerId =: userId
ORDER BY
CreatedDate
LIMIT :pageSize
OFFSET :offsetValue
];
}
public List<Case> selectCasesBySearchKeyWord(String searchKeyWord) {
searchKeyWord = '%' + searchKeyWord +'%';
return [
SELECT
Owner_Queue_Name__c,
CaseNumber,
CreatedDate,
Status,
Account.Name,
Priority,
Origin,
LastModifiedBy.Name
FROM
Case
WHERE
CaseNumber Like :searchKeyWord
OR
Status Like :searchKeyWord
Limit 10
];
}
}
CaseWrapper
I know a new question must be arising in your mind - Why are we following this type of backend architecture?
Let's suppose we are passing data directly to UI without wrapping then we have to do multiple server-side API calls to get each object's records and that will increase the processing & time.
This will also increase the complexity of the code and make it difficult to maintain.
Our aim is to create more efficient and standardized code……..
The wrapper class makes the code much more maintainable with less complexity. It also allows us to combine data from multiple objects together and send them all together in one go, ultimately minimizing the server-side API call to just one. It also allows us to add other objects' data in the future comfortably.
public with sharing class CaseWrapper {
@AuraEnabled
public String queueName;
@AuraEnabled
public String tabName;
@AuraEnabled
public Integer totalCases;
@AuraEnabled
public List<case> cases;
public CaseWrapper(String queueName, String tabName, List<case> queueCases, Integer totalCases) {
this.queueName = queueName;
this.tabName = tabName;
this.totalCases = totalCases;
this.cases = queueCases;
}
public CaseWrapper(String queueName, String tabName, List<case> queueCases) {
this.cases = queueCases;
}
}
CaseUtility
We are doing all the DML operations in the Utility class. It encapsulates the update operations and provides a cleaner way to handle these tasks in your code.
public with sharing class CaseUtility {
public static void updateMyCaseOwner(String Id) {
if (Schema.sObjectType.Case.fields.OwnerId.isUpdateable()) {
Case myCase = new Case();
myCase.Id = Id;
myCase.OwnerId = UserInfo.getUserId();
CaseUtility.updateCase(myCase);
}
}
public static void updateCase(Case updateCase) {
Database.update(updateCase, false);
}
}
FICCaseWrapper
In this class, we are wrapping cases with respective drop-down options.
public with sharing class FICCaseWrapper {
@AuraEnabled
public List<String> dropdownOptions;
@AuraEnabled
public List<CaseWrapper> wrappedCases;
public FICCaseWrapper(List<CaseWrapper> wrappedCases, List<String> dropdownOptions) {
this.dropdownOptions = dropdownOptions;
this.wrappedCases = wrappedCases;
}
}
Backend Data: JSON Format
This is how data is sent from the backend to the user interface. Below is the attached JSON object that the UI/LWC component is going to receive.
{
"dropdownOptions": [
"FFS – Entities",
"AVAF – Individuals",
"AVAF–Entities",
"My Cases",
"FFS – Individuals"
],
"wrappedCases": [
{
"cases": [
{
"Owner_Queue_Name__c": "AVAF Entities Remediation",
"CaseNumber": "00001000",
"CreatedDate": "2023-05-15T10:32:23.000Z",
"Status": "Closed",
"AccountId": "0015i00000kOM2vAAG",
"Priority": "High",
"Origin": "Phone",
"LastModifiedById": "0055i000008OPlpAAG",
"Id": "5005i00000U5kXUAAZ",
"Account": {
"Name": "Edge Communications",
"Id": "0015i00000kOM2vAAG"
},
"LastModifiedBy": {
"Name": "Ashish kumar",
"Id": "0055i000008OPlpAAG"
}
}
],
"queueName": "AVAF Entities Remediation",
"tabName": "Remediation",
"totalCases": 1
},
{
"cases": [
{
"Owner_Queue_Name__c": "AVAF Entities New Request",
"CaseNumber": "00001003",
"CreatedDate": "2023-05-15T10:32:23.000Z",
"Status": "Closed",
"AccountId": "0015i00000kOM31AAG",
"Priority": "Low",
"Origin": "Web",
"LastModifiedById": "0055i000008OPlpAAG",
"Id": "5005i00000U5kXXAAZ",
"Account": {
"Name": "Express Logistics and Transport",
"Id": "0015i00000kOM31AAG"
},
"LastModifiedBy": {
"Name": "Ashish kumar",
"Id": "0055i000008OPlpAAG"
}
}
],
"queueName": "AVAF Entities New Request",
"tabName": "New Request",
"totalCases": 1
},
{
"cases": [ ],
"queueName": "AVAF Entities Unassigned",
"tabName": "Unassigned",
"totalCases": 0
},
{
"cases": [ ],
"queueName": "AVAF Entities Awaiting Documents",
"tabName": "Awaiting Documents",
"totalCases": 0
},
{
"cases": [ ],
"queueName": "AVAF Entities Assigned",
"tabName": "Assigned",
"totalCases": 0
},
{
"cases": [ ],
"queueName": "AVAF Entities Archive",
"tabName": "Archive",
"totalCases": 0
},
{
"cases": [ ],
"queueName": "AVAF Entities Junk",
"tabName": "Junk",
"totalCases": 0
}
}
In Part 3, we'll talk about UI architecture.
Happy Coding! You can leave a comment to help me understand how the blog helped you. If you need further assistance, please contact us. You can click "Reach Us" on the website and share the issue with me.
Blog Credit:
A. Kumar
Salesforce Developer
Avenoir Technologies Pvt. Ltd.
Reach us: team@avenoir.ai
Comments