Friday, 24 January 2025

Quickly Creating Cases from the Salesforce Service Console Using Utility Actions.

Call center agents often face the challenge of efficiently capturing information while on a call, especially when they need to quickly create a case for a customer. To streamline this process, I developed a Lightning Web Component (LWC) that is launched as a utility action within the Salesforce Service Console. This solution enables agents to efficiently handle customer inquiries without losing context or valuable time.

Use Case

In a typical scenario:

  • Dual-Monitor Setup: Call center agents often use two monitors—one for the primary console application and another for utility tools.
  • Customer Interaction: While on a call, an agent may need to search for a customer's contact record using personally identifiable information (PII) like Social Security Number (SSN), Date of Birth (DOB), or Name.
  • System Performance Delays: Contact records may take time to load due to backend processes or system latency. In the meantime, the agent still needs a place to capture customer details and case information as they gather it on the phone.
  • Seamless Case Creation: Once the contact record is loaded, the agent can associate the case with the correct contact and save it instantly.

This utility action allows agents to:

  1. Launch a standalone window for capturing case details.
  2. Associate the case with a contact once the record is retrieved.
  3. Handle errors gracefully when attempting to save a case for invalid or non-contact objects.
  4. Window can also be minimized if needed.

Solution Overview

To implement this functionality, I created:

  1. An Aura Component: Acts as a wrapper to ensure the contact record's ID is reliably passed to the LWC. Using Aura as a wrapper provides consistent access to the recordId, addressing scenarios where LWC alone may fail to retrieve it.
  2. A Lightning Web Component (LWC): Handles the UI and business logic for capturing case details and associating the case with the contact.
  3. Error Handling: Added robust error handling to prevent saving a case if the current context is invalid (e.g., if the agent is viewing a non-contact record).

Key Features

  1. Utility Action Launch:
    • The utility application can be opened in a separate window, allowing agents to multitask efficiently.
  2. Dynamic Contact Association:
    • Agents can search for contacts using PII like SSN, DOB, or Name while capturing case details.
    • Once the contact record is loaded, the agent can link the case to the appropriate contact.
  3. Error Handling:
    • If the save button is clicked while the agent is not viewing a contact record, the system displays a clear error message, preventing incorrect associations.
  4. Performance-Friendly Design:
    • Captures case details even while waiting for the contact record to load, ensuring no data is lost during customer interactions.

Benefits of the Utility Action

  1. Improved Agent Efficiency: Enables agents to multitask and capture data without waiting for the system to load.
  2. Accurate Data Entry: Prevents errors by ensuring cases are associated with valid contact records.
  3. Enhanced User Experience: Provides a seamless, intuitive interface that fits naturally into the agent's workflow.

Why Use Aura as a Wrapper?

While LWC is a powerful framework, there are known inconsistencies when accessing the recordId directly in some scenarios. By using an Aura component as a wrapper:

  • The recordId is consistently available and passed to the LWC.
  • It ensures seamless integration between the Service Console application and the utility action.

Technical Implementation

Here is a high-level summary of the components:

Aura Component

  • Captures the recordId from the current context.
  • Passes the recordId to the LWC.
<aura:component implements="force:hasRecordId,flexipage:availableForAllPageTypes,lightning:utilityItem">
    <aura:attribute name="recordId" type="String" />
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
<c:createCaseUtility contactId="{!v.recordId}"/>
</aura:component>

({
    doInit: function (component, event, helper) {
        var recordId = component.get("v.recordId");
        if (!recordId) {
            console.warn("Record ID is undefined. Ensure the utility is launched from a record context.");
        } else {
            console.log("Record ID retrieved:", recordId);
        }
    }
});

LWC Component

  • Displays a user-friendly interface for capturing case details.
  • Handles case creation and error scenarios gracefully.
<template>
    <lightning-card>
        <div class="slds-m-left_small slds-m-right_small">
            <!-- First Line: Case Number and Reason Code -->
            <div class="slds-grid slds-gutters">
                <div class="slds-col slds-size_1-of-2">
                     <lightning-input type="text" label="Contact Name"
value={contactName} disabled></lightning-input>
                </div>
                <div class="slds-col slds-size_1-of-2">
                     <lightning-input type="text" name = "Subject" label="Subject"
value={subject} data-field="Subject"></lightning-input>
                </div>
               
            </div>
            <div class="slds-grid slds-gutters slds-m-top_small">
                <div class="slds-col slds-size_1-of-2">
                    <lightning-combobox
                        name="Status"
                        label="Status"
                        value={caseStatus}
                        placeholder="Select"
                        options={statusOptions}
                        onchange={handleInputChange}
                        data-field="CaseStatus">
                    </lightning-combobox>
                </div>
                <div class="slds-col slds-size_1-of-2">
                    <lightning-combobox
                        name="Case Origin"
                        label="Case Origin"
                        value={caseOrigin}
                        options={caseOriginOptions}
                        onchange={handleInputChange}
                        data-field="CaseOrigin"
                        >
                    </lightning-combobox>
                </div>
            </div>
            <!-- Second Line: Case Status and Due Date -->
            <div class="slds-grid slds-gutters slds-m-top_small">
               
                <div class="slds-col slds-size_1-of-2">
                    <lightning-combobox
                        name="Case Reason"
                        label="Case Reason"
                        value={caseReason}
                        options={caseReasonOption}
                        onchange={handleInputChange}
                        data-field="CaseReason"
                        >
                    </lightning-combobox>
                </div>
                <div class="slds-col slds-size_1-of-2">
                    <lightning-combobox
                        name="Case Type"
                        label="Case Type"
                        value={caseType}
                        options={caseTypeOptions}
                        onchange={handleInputChange}
                        data-field="CaseType"
                        >
                    </lightning-combobox>
                </div>
            </div>

            <!-- Third Line: Clinical Description -->
            <div class="slds-m-top_small">
                <lightning-textarea
                    label="Description"
                    value={description}
                    onchange={handleInputChange}
                    data-field="Description">
                </lightning-textarea>
            </div>

            <!-- Buttons aligned to the right -->
            <div class="slds-m-top_medium slds-text-align_right">
               <!-- <lightning-button
                    label="Cancel"
                    variant="neutral"
                    onclick={resetForm}>
                </lightning-button>-->
                <lightning-button class="slds-m-left_small"
                    label="Save"
                    variant="brand"
                    onclick={handleSave}>
                </lightning-button>
               
            </div>
        </div>
    </lightning-card>
</template>

JavaScript Controller
import { LightningElement,wire,api } from 'lwc';
import createCase from '@salesforce/apex/CreateCaseController.createCase';
import {ShowToastEvent} from "lightning/platformShowToastEvent";
import { minimize, EnclosingUtilityId } from 'lightning/platformUtilityBarApi';
import { getRecord } from 'lightning/uiRecordApi';
const CONTACT_FIELDS = ['Contact.Name','Contact.Phone','Contact.AccountId'];
export default class CreateCaseUtility extends LightningElement {
    contactName;
    contactPhone;
    accountId;
    caseOrigin;
    caseType;
    description;
    caseReason;
    caseStatus;
    subject;  

   @wire(EnclosingUtilityId) utilityId;
   
    currentRecordId;
    @api
    set contactId(value) {
        if (value !== this.currentRecordId) {
            this.currentRecordId = value;
            //this.validateCaseId();
        }
    }

    get contactId() {
        return this.currentRecordId;
    }
     get caseOriginOptions() {
      return [
          { label: 'Phone', value: 'Phone' },
          { label: 'Email', value: 'Email' },
          { label: 'Web', value: 'Web' }
         
      ];
   }
    get statusOptions() {
      return [
          { label: 'New', value: 'New' },
          { label: 'Working', value: 'Working' },
          { label: 'Escalated', value: 'Escalated' }
         
         
      ];
   }
   get caseReasonOption() {
      return [
          { label: 'Installation', value: 'Installation' },
          { label: 'Feedback', value: 'Feedback' },
          { label: 'Performance', value: 'Performance' }
         
         
      ];
   }
   get caseTypeOptions() {
      return [
          { label: 'Mechanical', value: 'Mechanical' },
          { label: 'Structural', value: 'Structural' },
          { label: 'Electronic', value: 'Electronic' }
         
         
      ];
   }

   @wire(getRecord, { recordId: '$currentRecordId', fields: CONTACT_FIELDS })
    wiredCase({ error, data }) {
        if (data) {
            this.contactName = data.fields.Name.value;
            this.contactPhone = data.fields.Phone.value;
            this.accountId = data.fields.AccountId.value;
           
        } else if (error) {
       
            console.error('Error fetching Case Number:', error);
        }
    }

     // add handleInputChange method to handle input changes.
    handleInputChange(event){
        const field = event.target.dataset.field;
        const value = event.target.value;
        console.log(field, value);
        switch(field){
            case 'CaseOrigin':
                this.caseOrigin = value;
                break;
            case 'CaseType':
                this.caseType = value;
                break;
            case 'Description':
                this.description = value;
                break;
            case 'CaseReason':
                this.caseReason = value;
                break;
            case 'CaseStatus':
                this.caseStatus = value;
                break;
            case 'Subject':
                this.subject =  value;
                break;
            default:
                break;}
    }

     resetForm() {
        //this.currentRecordId = '';
        this.caseStatus = '';
        this.caseReason = '';
        this.description = '';
        this.caseType = '';
        this.caseOrigin = '';
        this.handleMinimize();

    }
    async handleMinimize() {
        try {
            if (!this.utilityId) {
                return;
          }
        // Minimize the utility bar panel
        const isMinimized = await minimize(this.utilityId);
        console.log(`Minimize utility ${isMinimized ? 'successfully' : 'failed'}`);
        }
        catch (error) {
            // handle error
        }
    }
     showToast(title, message, variant) {
        const evt = new ShowToastEvent({
            title,
            message,
            variant,
        });
        this.dispatchEvent(evt);
    }
     validateCaseId() {
        if (!this.currentRecordId || !this.currentRecordId.startsWith('003')) {
            this.showToast('Case Create','Case should be created from Contact','error');
            return
        }
    }

    handleSave() {
        //this.validateCaseId()
        /*if (!this.currentRecordId) {
            this.showToast('Error', 'Case ID is missing in the URL!', 'error');
            return;
        }*/
        if (!this.currentRecordId || !this.currentRecordId.startsWith('003')) {
            this.showToast('Case Create','Case should be created from Contact','error');
            return
        }

    console.log('Handle save menthod caserecordid:', this.currentRecordId);
        const caseRecord = {

            Type: this.caseType,
            Status: this.caseStatus,
            Origin: this.caseOrigin,
            Description: this.description,
            ContactId: this.currentRecordId,
            Reason: this.caseReason,
            AccountId: this.accountId,
            ContactPhone: this.contactPhone,
            Subject: this.subject
             // Replace with the RecordTypeId for "Note"
        };
        console.log('Creating task with record type:',caseRecord);
        createCase({ cse: caseRecord })
            .then(() => {
                this.showToast('Success', 'Case created successfully!', 'success');
                this.resetForm();
               
            })
            .catch((error) => {
                this.showToast('Error', 'Error creating task: ' +
error.body.message, 'error');
            });
    }
}

Apex Class
public class CreateCaseController { @AuraEnabled public static void createCase(Case cse){ Insert cse; } }




No comments: