Empowering Approvers with LWCs & Templates

Photo of author
Written By Jean-Michel Tremblay
Salesforce Consultant

So you’ve setup Salesforce Advanced Approvals (or regular approvals for that matter), approval rules and conditions are setup as expected, you’ve tested in sandbox and deployed to production. Now comes real-world feedback. The main feedback you’ll get from sales reps will be the approvals take too long to complete (Source : Trust me).

In this post we’ll look at 2 options you can leverage to give your approvers more data so you can get those approvals faster everyday will then be happier.

First option being to add more data to your Visualforce email template, we’ll look at how we can add a table containing the quotes lines from the quote to be approved.

Visualforce email template for quote approval
Approval Request Email Template

Second option will be how we can create a lightning web component that will show a summary table of quotes lines subtotal and discount by product family.

Lightning web component that displays quote line summary by product family
Quote Line Summary by Product Family

The goal here is to maintain velocity in the approval process, reduce context switching for approvers, and ultimately, mitigate the financial and operational impact of approval delays. By providing approvers with more information upfront, we can expedite the decision-making process, enhance overall business efficiency, and reduce the cost of delayed decisions.

While this is geared towards Salesforce’s Advanced Approvals, the information shared here could easily extend standard approvals against any object within Salesforce.

Visualforce email template

If you’ve already setup Salesforce Advanced Approvals you had to create a Visualforce email templates for the different emails that can go out throughout the approval process. (Request, Recall, Approval and Rejection)

Approval Rule - Email template setup
Approval Rule – Email template setup

You can keep those templates fairly simple and only use fields from the quote but as we said earlier we want to keep momentum through the approval process. To do this we’ll want to share as much information as we can on the approval request email.

I tried to keep the email template fairly simple but wanted to expose the following.

  • Link to Quote
  • Account and Opportunity Name
  • List Total and Net Total
  • Average Customer Discount %
  • Quote Lines

How do we get there? You’ll need a couple things :

  • Visualforce email template (Obviously)
  • Apex Class
    • To retrieve the details from the quote lines
  • Visualforce Component
    • To display the Quote Line Items as a table in the approval request email. This component leverages the Apex Class to retrieve the data and formats it into a neatly structured table.

Below is what you get with the code shared at the bottom of the article, feel free to modify it as needed.

Quote email template example
Approval Request – Email example

Quote Line Summary – LWC

While I was building the VF email template, I was looking for other ways to make approvals easier and to share more details about the quote for users and approvers. I setup this lightning web component that shows a summary of quote lines grouped by product family.

Quote Line Summary - Lightning Web Component
Quote Line Summary – Lightning Web Component

I tested with a custom lightning web component that summarizes the quotes line on the quote by product family to show total list price, net price and calculates the discount applied to that product family.

It’s a fairly simple example that could obviously be further customized to add more details as required but it lines up to the objective of giving better information to approvers to allow for quicker decision making.

The LWC is built the standard way, HTML and JS as well as an Apex Class to retrieve the data. I’ve added conditional formatting based on the discount level. (Green 0-10%, Yellow 10-30%, Red 30%+) The levels are hardcoded but we create custom settings and pull the levels from there to not be code dependant if those were to change.

I’ve added the code below for the LWC as well.

Conclusion

As we wrap up this post, it’s essential to underline a few points. While the code snippets and Salesforce processes outlined here serve as guides, they’re not one-size-fits-all solutions.

I’m not a developer, and this code should serve as a basis for understanding the potential improvements you can make to your Salesforce approval processes. I would advise you to review, understand, and adapt this code according to your specific business needs.

Thanks for reading and I hope this post gives you ideas for enhancing your own approval processes.

Code Examples

VF Email Template

Visualforce email template

<messaging:emailTemplate subject="Quote Submitted for Approval" recipientType="User" relatedToType="sbaa__Approval__c">
    <messaging:plainTextEmailBody >
        Quote {!relatedTo.Quote__r.Name} has been submitted for approval by {!relatedTo.Quote__r.SBQQ__SalesRep__c}
    </messaging:plainTextEmailBody>
    <messaging:htmlEmailBody >
        <html>
            <head>
                <style>
                    table {
                        width: 100%;
                        border-collapse: collapse;
                    }
                    th, td {
                        border: 1px solid #dddddd;
                        text-align: left;
                        padding: 8px;
                    }
                    th {
                        background-color: #f2f2f2;
                    }
                </style>
            </head>
            <body>
                <p>
                    Quote <a href="{!URLFOR('/' + relatedTo.Quote__r.Id)}">{!relatedTo.Quote__r.Name}</a> has been submitted for approval by 
                    {!relatedTo.Quote__r.SBQQ__SalesRep__r.FirstName} {!relatedTo.Quote__r.SBQQ__SalesRep__r.LastName}
                </p>
                <p>
                    Account Name: {!relatedTo.Quote__r.SBQQ__Account__r.Name}<br/>
                    Opportunity Name: {!relatedTo.Quote__r.SBQQ__Opportunity2__r.name}<br/>
                    List Amount: {!relatedTo.Quote__r.SBQQ__ListAmount__c}<br/>
                    Average Customer Discount: {!relatedTo.Quote__r.SBQQ__AverageCustomerDiscount__c}<br/>
                    Net Amount: {!relatedTo.Quote__r.SBQQ__NetAmount__c}
                    
                </p>
                <c:QuoteLineItemsTable approvalRecordId="{!relatedTo.Id}"/>
                <p>
                    Actions: 
                    <a href="{!URLFOR('/apex/sbaa__Approve', null, ['id' = relatedTo.Id])}">Approve</a>
                    <a href="{!URLFOR('/apex/sbaa__Reject', null, ['id' = relatedTo.Id])}">Reject</a>
                </p>
            </body>
        </html>
    </messaging:htmlEmailBody>
</messaging:emailTemplate>

Apex Class

public class QuoteLineItemsEmailController {
    public Id approvalId { get; set {
        approvalId = value;
        loadQuoteLineItems();
    } }
    public List<SBQQ__QuoteLine__c> quoteLineItems { get; set; }

    public void loadQuoteLineItems() {
        sbaa__Approval__c approval = [SELECT Quote__c FROM sbaa__Approval__c WHERE Id = :approvalId];
        Id quoteId = approval.Quote__c;

        quoteLineItems = [SELECT Name, SBQQ__ProductName__c, SBQQ__Quantity__c, SBQQ__ListPrice__c, SBQQ__NetPrice__c, SBQQ__NetTotal__c
                          FROM SBQQ__QuoteLine__c
                          WHERE SBQQ__Quote__c = :quoteId];
    }
}

VF Component

<apex:component controller="QuoteLineItemsEmailController" access="global">
    <apex:attribute name="approvalRecordId" description="Approval ID" type="Id" assignTo="{!approvalId}" />
    <apex:outputPanel id="lineItemsPanel" layout="none" rendered="{!NOT(ISNULL(quoteLineItems))}">
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Product</th>
                    <th>QTY</th>
                    <th>List Price</th>
                    <th>Net Price</th>
                    <th>Net Total</th>
                </tr>
            </thead>
            <tbody>
                <apex:repeat var="line" value="{!quoteLineItems}">
                    <tr>
                        <td>{!line.Name}</td>
                        <td>{!line.SBQQ__ProductName__c}</td>
                        <td>{!line.SBQQ__Quantity__c}</td>
                        <td>{!line.SBQQ__ListPrice__c}</td>
                        <td>{!line.SBQQ__NetPrice__c}</td>
                        <td>{!line.SBQQ__NetTotal__c}</td>
                    </tr>
                </apex:repeat>
            </tbody>
        </table>
    </apex:outputPanel>
</apex:component>

Lightning Web Component

Apex Class

public with sharing class QuoteLineSummaryController {
    @AuraEnabled(cacheable=true)
    public static List<QuoteLineSummaryData> getQuoteLineSummary(Id quoteId) {
        List<QuoteLineSummaryData> summaryData = new List<QuoteLineSummaryData>();

        // Your SOQL query to fetch the required data from SBQQ__QuoteLine__c object
        String queryString = 'SELECT SBQQ__ProductFamily__c, SBQQ__Quantity__c, SBQQ__ListTotal__c, SBQQ__NetTotal__c ' +
                             'FROM SBQQ__QuoteLine__c WHERE SBQQ__Quote__c = :quoteId';

        List<SBQQ__QuoteLine__c> quoteLines = Database.query(queryString);
        Map<String, QuoteLineSummaryData> productFamilyMap = new Map<String, QuoteLineSummaryData>();

        for (SBQQ__QuoteLine__c quoteLine : quoteLines) {
            String productFamily = quoteLine.SBQQ__ProductFamily__c;
            if (!productFamilyMap.containsKey(productFamily)) {
                productFamilyMap.put(productFamily, new QuoteLineSummaryData(productFamily));
            }
            QuoteLineSummaryData row = productFamilyMap.get(productFamily);
            row.quantity += quoteLine.SBQQ__Quantity__c;
            row.listPrice += quoteLine.SBQQ__ListTotal__c;
            row.netPrice += quoteLine.SBQQ__NetTotal__c;
            row.calculateDiscountPercentage();
        }

        summaryData = productFamilyMap.values();
        return summaryData;
    }

    public class QuoteLineSummaryData {
        @AuraEnabled
        public String productFamily;
        @AuraEnabled
        public Decimal quantity;
        @AuraEnabled
        public Decimal listPrice;
        @AuraEnabled
        public Decimal netPrice;
        @AuraEnabled
        public Decimal discountPercentage;

        public QuoteLineSummaryData(String productFamily) {
            this.productFamily = productFamily;
            this.quantity = 0;
            this.listPrice = 0;
            this.netPrice = 0;
            this.discountPercentage = 0;
        }
        public void calculateDiscountPercentage() {
            if (listPrice != 0) {
                this.discountPercentage = (listPrice - netPrice) / listPrice * 100;
            } else {
                this.discountPercentage = 0;
            }
        }
    }
}

LWC – HTML

<template>
    <lightning-card title="Quote Line Summary">
        <div class="slds-m-around_medium">
            <table class="slds-table slds-table_bordered slds-no-row-hover slds-table_cell-buffer">
                <thead>
                    <tr>
                        <th scope="col"><div class="slds-truncate">Product Family</div></th>
                        <th scope="col"><div class="slds-truncate">Quantity</div></th>
                        <th scope="col"><div class="slds-truncate">List Price</div></th>
                        <th scope="col"><div class="slds-truncate">Net Price</div></th>
                        <th scope="col"><div class="slds-truncate">Discount</div></th>
                    </tr>
                </thead>
                <tbody>
                    <template for:each={quoteLineSummaryData} for:item="row">
                        <tr key={row.key} class={row.rowClass}>
                            <td>{row.productFamily}</td>
                            <td>{row.quantity}</td>
                            <td>{row.listPrice}</td>
                            <td>{row.netPrice}</td>
                            <td>{row.discountPercentage}</td>
                        </tr>
                    </template>
                </tbody>
            </table>
        </div>
    </lightning-card>
</template>

LWC – Javascript

import { LightningElement, wire, api, track } from 'lwc';
import getQuoteLineSummary from '@salesforce/apex/QuoteLineSummaryController.getQuoteLineSummary';
import { refreshApex } from '@salesforce/apex';

export default class QuoteLineSummary extends LightningElement {
    @api recordId;
    @track quoteLineSummaryData = [];
    wiredResult;

    currencyFormatter = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD',
    });

    percentageFormatter = new Intl.NumberFormat('en-US', {
        style: 'percent',
        minimumFractionDigits: 2,
        maximumFractionDigits: 2,
    });

    @wire(getQuoteLineSummary, { quoteId: '$recordId' })
    wiredQuoteLineSummary(result) {
        this.wiredResult = result;
        const { data, error } = result;
        if (data) {
            this.quoteLineSummaryData = data.map(row => {
                const discountPercentage = row.discountPercentage / 100;
                let rowClass = '';
                if (discountPercentage >= 0 && discountPercentage <= 0.1) {
                    rowClass = 'slds-theme_success';
                } else if (discountPercentage > 0.1 && discountPercentage <= 0.3) {
                    rowClass = 'slds-theme_warning';
                } else {
                    rowClass = 'slds-theme_error';
                }

                return {
                    ...row,
                    listPrice: this.currencyFormatter.format(row.listPrice),
                    netPrice: this.currencyFormatter.format(row.netPrice),
                    discountPercentage: this.percentageFormatter.format(discountPercentage),
                    rowClass,
                };
            });
        } else if (error) {
            console.error('Error retrieving quote line summary data:', error);
        }
    }

    connectedCallback() {
        return refreshApex(this.wiredResult);
    }
}

2 thoughts on “Empowering Approvers with LWCs & Templates”

  1. Thank you for sharing your work. We have forwarded the code to our developers and are eagerly anticipating the implementation of this solution. Great stuff!

  2. Thank you for sharing your work. We have forwarded the code to our developers and are eagerly anticipating the implementation of this solution. Great stuff!

Comments are closed.