Salesforce1 Demo: Building an Expense Tracker - El Toro - Find articles about Visualforce, Apex, Force.com and Salesforce in general

Print Preview

Salesforce1 Demo: Building an Expense Tracker

Look mom... No Objective-C or Java needed to build mobile applications!

This article will guide you through the creation of a mobile application using the new Salesforce1 platform, which allows you to use just Visualforce pages and Apex controllers and it's device independent.

You may have already heard of Salesforce1, which was released early today at Dreamforce ’13. At the beginning, I was not too impressed with the application because Salesforce have already developed and distributed some very nice applications for mobile devices (Chatter, Touch, Logger, …) until I realized this is not just another application but an actual platform!
 
You can customize this application by building your own user interface with page layouts, actions, and if you need more advanced user interfaces, you can build Visualforce pages.
 
You have already experienced how easy is to build applications using clicks and a bit of code, but those applications have only been running on desktop (non-mobile devices) systems until today. This is going to be the real revolution!
 
I had wanted to build a Salesforce application that runs on mobile devices, but my only options had been using the Salesforce SDK. Don’t take me wrong the Salesforce SDK is great, powerful and simple but it requires you to code in Objective-C or Java and build the entire application yourself. But really, do we need to go through this much… There must be a better way! That is exactly what was announced today with Salesforce1
 
As soon as I heard about this new platform, I got really excited and decided to finally build the application. I am very impressed at how easy, and fast it was to develop and how beautiful it works on my mobile device.
 
This article will show you how I built such application, but before we get started, let me tell you what I built.
 
Some of you who have already met me in person know that “El Toro” is a Salesforce instructor who travels a lot teaching the developer courses; therefore I have to submit many expense reports. My main issue is that the credit card company must report those expenses before I can create the expense report. This may not sound bad, but I have to keeping track of a lot of paper receipts, remembering what each receipt was for, reading the receipts which degrade after a while, …
 
The application I will show you how I built helps me keep track of the expenses, fill out some fields (local amount, reason, payment type, geo-location, …), and attaching a picture of the receipt.

Note: At the bottom of this article, I have included some information as how you can install an unmanaged package (so you can see and edit the code) with this application.

Trips List

The first thing you see when you open Salesforce1 is a Visualforce page listing the different trips. This screen has two sections, one for the trips that have not been submitted and one for the trips that have already been processed:

The reason this page is shown first is that I put it on the top in the navigation (Left Nav) panel.

How did I make this be the default, landing page?

Go to Setup > Administer > Mobile Administration > Mobile Navigation and add the Visualforce tab as the first item in the list.

How did I make this tab?

Go to Setup > Build > Create > Tabs, and create a mobile tab for a Visualforce page

1. Give it a short label
2. Select the content
3. Define the style if needed
4. Make sure to enable the “Mobile Ready”

How did I make this Visualforce page?

Create this Visualforce page, and make sure the “Available for Salesforce mobile apps” is checked.

<apex:page docType="html-5.0" applyBodyTag="false" applyHtmlTag="false" cache="true" showHeader="false" standardStylesheets="true"
    standardController="Trip__c" extensions="sf1Tab_ViewTrips" >
<html> 
<head> 
    <title>View Trips</title>
    <meta charset="utf-8" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    
    <!-- jQuery Mobile -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    <apex:stylesheet value="https://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.css" />
    <apex:includeScript value="https://code.jquery.com/jquery-1.9.1.min.js"/>
    <apex:includeScript value="https://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.js"/>
    
    <!-- Salesforce1 -->
    <apex:includeScript value="/canvas/sdk/js/publisher.js" />

    <!-- My own scripts -->
    <script>
        var sf1Data;
        $j = jQuery.noConflict();
        Visualforce.remoting.timeout = 120000;
        
        // Process submit button clicked
        Sfdc.canvas.publisher.subscribe({name: "publisher.post", onData:function(e) {
             alert('Button has been disabled and should not be clicked!');
        }});
    
        $j(document).ready(function() {
            // Disable submit button
            Sfdc.canvas.publisher.publish({name: "publisher.setValidForSubmit", payload:"false"});
        });
    </script>
</head> 
<body>
<div data-role="page">
    <div data-role="content">
        <div data-role="popup" id="statusPopupID" class="ui-content" data-overlay-theme="a">
            Please Wait...<br/>
            <img src="{!$Resource.ProgressBar}"/>
        </div>
        <form>
            <div data-role="collapsible" data-collapsed="false" data-content-theme="c">
                <h3><div style="white-space:normal">Current Trips</div></h3>
                <p>
                    <c:sf1Tab_ViewTrips_Trips Trips="{!currentTrips}"/>
                </p>
            </div>
            
            <div data-role="collapsible" data-collapsed="true" data-content-theme="c">
                <h3><div style="white-space:normal">Recently Submitted Trips</div></h3>
                <p>
                    <c:sf1Tab_ViewTrips_Trips Trips="{!pastTrips}"/>
                </p>
            </div>
        </form>
Version #1.1<br/>
    </div><!-- /content -->
</div><!-- /page -->
</body>
</html>
</apex:page>

Which uses this extension:

public with sharing class sf1Tab_ViewTrips {
    public sf1Tab_ViewTrips(ApexPages.StandardController controller) { }

    public List<Trip__c> currentTrips {
        get {
            return FindTrips(false);
        }
    }

    public List<Trip__c> pastTrips {
        get {
            return FindTrips(true);
        }
    }

    private List<Trip__c> FindTrips(Boolean isSubmited) {
        String SOQL;

        SOQL = '';
        SOQL += 'SELECT Id,isActive__c,Local2Concur__c,Location__c,Name,StartDate__c,StopDate__c,SubmitedReport__c,Total_Expenses__c ';
        SOQL += 'FROM Trip__c ';
        SOQL += 'WHERE SubmitedReport__c = :isSubmited ';
        SOQL += 'ORDER BY StartDate__c DESC ';
        if (isSubmited) {
            SOQL += 'LIMIT 10 ';
        }
        return Database.query(SOQL);
    }
}

Trip Record Page

When you navigate to a trip, you will see this record page:

The record highlights section shows the most information summary information for this trip, which includes the trip name, the start and stop dates and a label indicating if this trip is active or not.

How did I configure the Record Highlights?

Go to Setup > Build > Create > Objects, Select the custom object (Trip in my case), then go to Compact Layouts where you’ll create a Compact Layout and assign the layouts to different profiles.

How did I configure the Record page?

The rest of the page is configured using the standard page layout editor, by going to Setup > Build > Create > Objects, Select the custom object (Trip in my case), then go to Page Layouts.

Publisher Actions

In page layout shown above, I configured the Publisher Actions, page details, and related lists. But what are these Publisher Actions? One of the major design patterns that you must learn to apply on a mobile project is to use those “micro-moments” your users are going to have. This is a major change on the design of your applications, where you are going to design a way for your users to action on their data in a very quick, short and simple way. You do not want to build large Visualforce pages with tons of interactivity but rather very simple and quick pages where your users will do the most repetitive tasks.
 
In my application, when you click on the Publisher Actions Button, you will see this screen showing you the actions I created for the trip object:
 

Make Active

When you select this Publisher Action, you will see this Screen:

Although I have not fully implemented this, I am thinking on putting an item on the Navigation Menu (left nav) to create an expense without having to select the trip first. In order to accomplish this, I need to have a “default” trip, which I am just calling the “active” trip. This screen allows me to activate a trip so it can be the default for that screen.
 
In the early stages of the Beta testing, Salesforce1 had a very slow performance on the Visualforce pages and I thought about this functionality, specially when I only had a 3G connection… I’ll probably build the page to quick create the expense when I get some time ;-)
 

How did I create the “Make Active” Publisher Action?

As shown above, I added the buttons to the Record details section on the Page Layout editor. But the button was created by going to Setup > Build > Create > Objects, Select the custom object (Trip in my case), then go to Buttons, Links, and Actions. Here I created a new action like this:

How did I make the “Make Active” Visualforce Page?

This is probably one of the most simple Visualforce pages in this project and the one that I built first when I was learning and understanding how to connect Salesforce1, Visualforce Pages and Apex controllers. As you can see, I do not use the standard way of invoking controllers, but rather the Visualforce Remoting way because I wanted to avoid having a large Viewstate.

<apex:page docType="html-5.0" applyBodyTag="false" applyHtmlTag="false" cache="true" showHeader="false" standardStylesheets="false"
    standardController="Trip__c" extensions="sf1Trip_MakeActive" >
<html> 
<head> 
    <title>Activate Trip</title>
    <meta charset="utf-8" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    
    <!-- jQuery Mobile -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    <apex:stylesheet value="https://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.css" />
    <apex:includeScript value="https://code.jquery.com/jquery-1.9.1.min.js"/>
    <apex:includeScript value="https://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.js"/>
    
    <!-- Salesforce1 -->
    <apex:includeScript value="/canvas/sdk/js/publisher.js" />

    <!-- My own scripts -->
    <script>
        var sf1Data;
        $j = jQuery.noConflict();
        Visualforce.remoting.timeout = 120000;
        
        // Process submit button clicked
        Sfdc.canvas.publisher.subscribe({name: "publisher.post", onData:function(e) {
             alert('Button has been disabled and should not be clicked!');
        }}); 
        
        function SubmitData() {
            $j('#statusPopupID').popup('open');
            sf1Trip_MakeActive.submitData(sf1Data, function(result, event) {
                $j('#statusPopupID').popup('close');
                if(event.status) {
                    if (result.isSuccess) {
                        Sfdc.canvas.publisher.publish({name: "publisher.close", payload:{refresh:"true"}});
                    } else {
                        alert(result.message);            
                    }
                } else {
                    alert("Visualforce Remoting Failed");
                }
            });
        }
    
        $j(document).ready(function() {
            // Disable submit button
            Sfdc.canvas.publisher.publish({name: "publisher.setValidForSubmit", payload:"false"});
            sf1Data = {"apexType":"c.sf1Trip_MakeActive.sf1Data"};
            sf1Data.TripID = '{!Trip__c.id}';
        });
    </script>
</head> 
<body>
<div data-role="page">
    <div data-role="content">
        <div data-role="popup" id="statusPopupID" class="ui-content" data-overlay-theme="a">
            Please Wait...<br/>
            <img src="{!$Resource.ProgressBar}"/>
        </div>
        <form>
            <div data-role="collapsible" data-collapsed="false" data-content-theme="c">
                <h3><div style="white-space:normal">{!Trip__c.Name}</div></h3>
                <p>When you make this trip active, all other active trips will be deactivated.</p>
            </div>
            <center><a href="javascript:SubmitData();" data-role="button" data-inline="true" data-mini="true" data-theme="b">Activate</a></center>
        </form>
Version #1.7<br/>
    </div><!-- /content -->
</div><!-- /page -->
</body>
</html>
</apex:page>

Do not forget to make this page available for mobile, or you will not be able to use it inside Salesforce1. This is the controller for the page, but remember… I am using Visualforce Remoting for the actions!
 

global with sharing class sf1Trip_MakeActive {
    public sf1Trip_MakeActive(ApexPages.StandardController controller) { }
    
    @RemoteAction
    global static sf1Result submitData(sf1Data data) {
        try {
            String TripID = data.TripID;
            
            String SOQL = '';
            SOQL += '';
            SOQL += 'SELECT ID, isActive__c ';
            SOQL += 'FROM Trip__c ';
            SOQL += 'WHERE isActive__c = TRUE OR id = :TripID ';
            
            List<Trip__c> trips = Database.query(SOQL);
            for (Trip__c trip : trips) {
                System.debug(trip);
                // If this is the current trip, then it should be active. Otherwise, deactivate it.
                Boolean isCurrent = (trip.id == TripID);
                trip.isActive__c = isCurrent;
            }
            update trips;
        } catch (Exception ex) {
            return new sf1Result(ex);
        }
        return new sf1Result(data.TripId); 
    }
    
    global class sf1Data {
        global String TripId { get; set; }
    }
}

To make development easier, I created a standard pattern for these pages, which includes returning the same result class: sf1Result. This is such class:

global class sf1Result {
    public Boolean isSuccess { get; set; }
    public String message { get; set; }
    public String recordID { get; set; }
    
    public sf1Result(Exception ex) {
        isSuccess = false;
        message = 'Apex Exception: ' + ex.getStackTraceString() + ' : ' + ex.getMessage();
    }

    public sf1Result(ID recordID) {
        isSuccess = true;
        message = 'Apex completed succesfully';     
        this.recordID = recordID;   
    }
    
    private sf1Result() {}
}

Add Expense

This is the most important page of the application, because it allows you to enter the data for the expense (the main purpose of the application). 

If the screen looks long, and you think this is not a real screen capture, and El Toro must of have done some Photoshop… You are correct. This page does require the user to scroll while entering the data… so I had to take several screen captures and put them together.
 
One of the cool things about HTML5 is that it allows you to enter the data very easy, because it allows you to configure how each field is going to be entered. These are some of the screens I took while creating an expense, note how there are different entry methods.
 

Once you have entered all the data, including if you are at the place of purchase (to send the geo-coordinates), you will submit the form by either clicking on the Save button at the bottom of the screen, or the submit button at the top of the page. While the data is being sent to Salesforce for saving it, a “Please Wait” message is displayed while the operation is being performed and before the expense detail record page is shown.

How did I create the “Add Expense” Visualforce page?

The page was basically created the same way as the previous page. I used jQuery Mobile for the page, I made it available for Mobile device, and created a Publisher Action on the page layout for the trip object. But obviously the code is very different, let me show you:

<apex:page docType="html-5.0" applyBodyTag="false" applyHtmlTag="false" cache="true" showHeader="false" standardStylesheets="false"
    standardController="Trip__c" extensions="sf1Trip_AddExpense" >
<html> 
<head> 
    <title>Add Expense</title>
    <meta charset="utf-8" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    
    <!-- jQuery Mobile -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    <apex:stylesheet value="https://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.css" />
    <apex:includeScript value="https://code.jquery.com/jquery-1.9.1.min.js"/>
    <apex:includeScript value="https://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.js"/>
    
    <!-- Salesforce1 -->
    <apex:includeScript value="/canvas/sdk/js/publisher.js" />

    <!-- Image Processor -->
    <script src="{!URLFOR($Resource.CanvasResize, 'jquery.exif.js')}"></script>
    <script src="{!URLFOR($Resource.CanvasResize, 'jquery.canvasResize.js')}"></script>
    <script src="{!URLFOR($Resource.CanvasResize, 'canvasResize.js')}"></script>

    <!-- My own scripts -->
    <script>
        var sf1Data;
        var LAT = null;
        var LONG = null;
        var fileResized;
        var fileOriginal;
        $j = jQuery.noConflict();
        var fileOriginalReader = new FileReader();
        Visualforce.remoting.timeout = 120000;
        
        // Process submit button clicked
        Sfdc.canvas.publisher.subscribe({name: "publisher.post", onData:function(e) {
            SubmitData();
        }}); 
        
        function SubmitData() {
            $j('#statusPopupID').popup('open');
        
            // Pick up values
            sf1Data.strIsReimbursable = $j('#Reimbursable').val();
            sf1Data.strPurchaseDateTimeGTM = $j('#PurchaseDateTime').val();
            // For debugging purposes...
            if (sf1Data.strPurchaseDateTimeGTM == "") {
                sf1Data.strPurchaseDateTimeGTM = "2013-10-28T00:34:42.251Z";
            }
            sf1Data.expense.ExpenseType__c = $j('#ExpenseType').val();
            sf1Data.expense.PaymentType__c = $j('#PaymentType').val();
            sf1Data.expense.Currency__c = $j('#Currency').val();
            sf1Data.expense.AmountLocal__c = $j('#LocalAmount').val();
            sf1Data.expense.Location__c = $j('#Location').val();
            sf1Data.expense.Description__c = $j('#Description').val();
            sf1Data.expense.Trip__c = '{!Trip__c.id}';
            if ($j('#POS').val() == "true") {
                if ((LAT != null) && (LONG != null)) {
                    sf1Data.expense.GPS__Latitude__s = LAT;
                    sf1Data.expense.GPS__Longitude__s = LONG;
                }
            }
            
            sf1Trip_AddExpense.submitData(sf1Data, function(result, event) {
                $j('#statusPopupID').popup('close');
                if(event.status) {
                    if (result.isSuccess) {
                        if (typeof sforce == 'undefined') {
                            alert('Record [' + result.recordID + '] has been added');
                        } else {
                            sforce.one.navigateToSObject(result.recordID);
                            Sfdc.canvas.publisher.publish({name: "publisher.close", payload:{refresh:"true"}});
                        }
                    } else {
                        alert(result.message);
                    }
                } else {
                    alert("Visualforce Remoting Failed");
                }
            });
        }
    
        $j(document).ready(function() {
            // Enable submit button
            // alert('Init');
            Sfdc.canvas.publisher.publish({name: "publisher.setValidForSubmit", payload:"true"});
            sf1Data = {"apexType":"c.sf1Trip_AddExpense.sf1Data"};
            sf1Data.expense = {};
            sf1Data.image = {"apexType":"c.sf1Expense_AddReceipt.sf1Data"};
            findLocation();
        });
        
        // Find GPS Location
        function findLocation() {
            if(navigator.geolocation) {
                    navigator.geolocation.getCurrentPosition (
                        // successFunction
                        function(position) {
                            LAT = position.coords.latitude;
                            LONG = position.coords.longitude;
                        },
                        // errorFunction
                        function(position) {
                            alert("Geolocation not available");
                        }
                    );
            } else {
                alert("navigator.geolocation is not available");
            }
        }
        
        // Process Image
        function fileChosen(fileChosenEvent) {
            // Get file
            fileOriginal = fileChosenEvent.files[0];
            
            // Is it an image?
            if(!fileOriginal.type.match('image')) {
                alert('Must use an image! Received: ' + fileOriginal.type);
                return;
            }
            
            // Process large image
            fileOriginalReader.readAsDataURL(fileOriginal);
        }
        
        fileOriginalReader.onload = function(fileOriginalReaderEvent) {
            // Display image
            // drawImage(fileOriginalReaderEvent.target.result);
            
            // Resize it!
            resizeImage();
        };
        
        function resizeImage() {    
            // Resize Image
            $j.canvasResize(fileOriginal, {
                width: 500,
                height: 0,
                crop: false,
                quality: 80,
                //rotate: 90,
                callback: function(data, width, height) {
                    sf1Data.image.name = fileOriginal.name;
                    sf1Data.image.contentType = fileOriginal.type;
                    sf1Data.image.sImage = data;
                    sf1Data.image.bodyLength = data.length;
                    // vfrData.bData = $j.canvasResize('dataURLtoBlob', data);
                    // drawImage(data);
                }
            });
        };

        function drawImage(data) {
            var span = document.createElement('span');
            span.innerHTML = ['<img class="popphoto" src="', data, '" title="', escape(fileOriginal.name), '" style="max-width:100%"/>'].join('');
            $j('#ImagePreview').html(span);
        }
    </script>
</head> 
<body>
<div data-role="page">
    <div data-role="content">   
        <div data-role="popup" id="statusPopupID" class="ui-content" data-overlay-theme="a">
            Please Wait...<br/>
            <img src="{!$Resource.ProgressBar}"/>
        </div>
        <form>
            <h3>Trip: {!Trip__c.Name}</h3>
            <label for="PurchaseDateTime">Purchase Date:</label>
            <input type="datetime-local" name="PurchaseDateTime" id="PurchaseDateTime" data-mini="true" />
            
            <label for="ExpenseType" class="select">Expense Type:</label>
            <select name="ExpenseType" id="ExpenseType" data-mini="true">
               <apex:repeat value="{!ExpenseTypes}" var="ExpenseType">
                   <option value="{!ExpenseType.value}">{!ExpenseType.label}</option>
               </apex:repeat>
            </select>
            
            <label for="PaymentType">Payment Type:</label>
            <select name="PaymentType" id="PaymentType" data-mini="true">
               <apex:repeat value="{!PaymentTypes}" var="PaymentType">
                   <option value="{!PaymentType.value}">{!PaymentType.label}</option>
               </apex:repeat>
            </select> 
            
            <label for="Currency">Currency:</label>
            <select name="Currency" id="Currency" data-mini="true">
               <apex:repeat value="{!Currencies}" var="Currency">
                   <option value="{!Currency.value}">{!Currency.label}</option>
               </apex:repeat>
            </select>
            
            <label for="LocalAmount">$ Local:</label>
            <input type="number" step="0.01" name="LocalAmount" id="LocalAmount" data-mini="true" />

            <label for="Location">Location:</label>
            <select name="Location" id="Location" data-mini="true">
                <option value="">-- None --</option>
                <option value="{!Trip__c.Location__c}">{!Trip__c.Location__c}</option>
                <option value="Home">Home</option>
                <option value="Transit (Plane, Airport)">Transit (Plane, Airport)</option>
            </select>

            <label for="Description">Description:</label>
            <textarea name="Description" id="Description"></textarea>
            
            <label for="Image">Receipt Image:</label>
            <input type="file" name="Image" id="Image" data-mini="true" onchange="fileChosen(this)"/>
            <div id="ImagePreview" />
            
            <label for="POS">Are you at the point of purchase?</label>
            <select name="POS" id="POS" data-role="slider" value="true">
                <option value="false">No</option>
                <option value="true" selected="true">Yes</option>
            </select>
            
            <label for="Reimbursable">Is Reimbursable</label>
            <select name="Reimbursable" id="Reimbursable" data-role="slider" value="true">
                <option value="false">No</option>
                <option value="true" selected="true">Yes</option>
            </select>
            
            <center><a href="javascript:SubmitData();" data-role="button" data-inline="true" data-mini="true" data-theme="b">Save</a></center>
        </form>
Version #2.7<br/>
    </div><!-- /content -->
</div><!-- /page -->
</body>
</html>
</apex:page>

As you can see in the code, this page does 2 interesting things. First, it allows you to enter a picture of the receipt and it resizes it before submitting it to Salesforce to be stored. Second, it gets the geo-coordinates and if the user selects he is at the place of purchase, then they are submitted to Salesforce.
 
This is the controller:
 

global with sharing class sf1Trip_AddExpense {
    public sf1Trip_AddExpense(ApexPages.StandardController controller) { }

    public List<SelectOption> ExpenseTypes {
        get {
            if (ExpenseTypes == null) {
                ExpenseTypes = new List<SelectOption>();
                ExpenseTypes.add(new SelectOption('', '-- None --'));
                for (ExpenseType__c t : [SELECT Id,Name FROM ExpenseType__c ORDER BY Sorter__c DESC]) {
                    ExpenseTypes.add(new SelectOption(t.ID, t.Name));
                }
            }
            return ExpenseTypes;
        }
        private set;
    }

    public List<SelectOption> PaymentTypes {
        get {
            if (PaymentTypes == null) {
                PaymentTypes = new List<SelectOption>();
                PaymentTypes.add(new SelectOption('', '-- None --'));
                Schema.DescribeFieldResult dfr = Expense__c.PaymentType__c.getDescribe();
                for (Schema.PicklistEntry ple : dfr.getPicklistValues()) {
                    PaymentTypes.add(new SelectOption(ple.getValue(), ple.getLabel(), !ple.isActive()));
                }
            }
            return PaymentTypes;
        }
        private set;
    }
    
    public List<SelectOption> Currencies {
        get {
            if (Currencies == null) {
                Currencies = new List<SelectOption>();
                Currencies.add(new SelectOption('', '-- None --'));
                Schema.DescribeFieldResult dfr = Expense__c.Currency__c.getDescribe();
                for (Schema.PicklistEntry ple : dfr.getPicklistValues()) {
                    Currencies.add(new SelectOption(ple.getValue(), ple.getLabel(), !ple.isActive()));
                }
            }
            return Currencies;
        }
        private set;
    }
    
    @RemoteAction
    global static sf1Result submitData(sf1Data data) {
        ID recordID;
        
        try {
            System.debug('Data Received: ' + data.expense);
            
            // Add Expense
            data.procesSpecialValues();
            insert data.expense;
            data.addImage();
            recordID = data.expense.id;
            System.debug('Data Processed');
        } catch (Exception ex) {
            return new sf1Result(ex);
        }
        return new sf1Result(recordID); 
    }
    
    global class sf1Data {
        global Expense__c expense { get; set; }
        global sf1Expense_AddReceipt.sf1Data image { get; set; }

        // special values
        global String strIsReimbursable { get; set; }
        global String strPurchaseDateTimeGTM { get; set; }

        public void addImage() {
            image.ParentId = expense.id;
            sf1Result result = sf1Expense_AddReceipt.submitData(image);
            if (!result.isSuccess) {
                throw new sf1Exception(result.message);
            }
        }
                
        public void procesSpecialValues() {
            System.debug('Is Reimbursable: ' + strIsReimbursable);
            expense.isReimbursable__c = Boolean.valueOf(strIsReimbursable);
            System.debug('Date: ' + strPurchaseDateTimeGTM);
            expense.PurchaseDate__c = getDate(strPurchaseDateTimeGTM).Date();
        }
        
        private DateTime getDate(String value) {
            // iPhone Format Received "2013-10-28T00:34:42.251Z", "2013-10-28T00:34:42Z"
            // Format Required  “yyyy-MM-dd HH:mm:ss”

            String input = value;
            input = input.replace('T', ' ');
            input = input.substring(0, 19);
            System.debug('Date before processor: ' + value);
            System.debug('Date after processor: ' + input);
            return DateTime.valueOfGMT(input);
        }
    }
}

Since I am allowing the user to add receipts to expenses in two places, where the expense is created and later in the expense itself, then I created a class that saves the receipt image as an attachment to the record. This is such class:

global class sf1Expense_AddReceipt {
    public sf1Expense_AddReceipt(ApexPages.StandardController controller) { }

    @RemoteAction
    global static sf1Result submitData(sf1Data data) {
        ID recordID;
    
        try {
            System.debug('Data Received');
            System.debug(data);
            if (data == null) {
                throw new sf1Exception('No data received!');
            } else if ((data.sImage == null) || (data.sImage.length() == 0)) {
                // throw new sf1Exception('Empty file received!');
            } else {
                // Good data received.
                String b64;
                String dataType;
                Attachment att;
                
                // Get Data
                List<String> docParts = data.sImage.split(',');
                String metadata = docParts[0];
                b64 = docParts[1];            
                List<String> metadataParts = metadata.split(';');
                dataType = metadataParts[0].split(':')[1];            
                data.bImage = EncodingUtil.base64Decode(b64);
            
                // Attach file
                att = new Attachment();
                att.Body = data.bImage;
                att.ContentType = data.contentType;
                att.Name = data.name;
                att.ParentId = data.ParentId;
                Insert att;
                recordID = att.id;
            }        
            System.debug('Data Processed');
        } catch (Exception ex) {
            return new sf1Result(ex);
        }
        return new sf1Result(recordID); 
    }
    
    global class sf1Data {
        public Blob bImage { get; set; }

        global String ParentId { get; set; }
        global String name { get; set; }
        global String sImage { get; set; }
        global String contentType { get; set; }
        global Integer bodyLength { get; set; }
    }
}

View Expenses

This screen allows the user to see the expenses for a specific trip. The screen looks like this:

How did I make this “View Expenses” Visualforce page?

Since I do not much jQuery or JavaScript, I did not know how to do a Partial Page Refresh without using Visualforce’s Rerender, which is very easy to do. So I decided for this page not to use Viualforce Remoting but rather use the standard way of Visualforce invoking methods in the controller from Javascript by using the <apex:actionfunction> tag. Plus, I later thought this would be a great way to demonstrate how to use both ways to invoke methods from Apex... So I decided to leave it like that.
 
This is the Visualforce page I created:

<apex:page docType="html-5.0" applyBodyTag="false" applyHtmlTag="false" cache="true" showHeader="false" standardStylesheets="false"
    standardController="Trip__c" extensions="sf1Trip_ViewExpenses" >
<html> 
<head> 
    <title>View Trips</title>
    <meta charset="utf-8" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    
    <!-- jQuery Mobile -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    <apex:stylesheet value="https://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.css" />
    <apex:includeScript value="https://code.jquery.com/jquery-1.9.1.min.js"/>
    <apex:includeScript value="https://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.js"/>
    
    <style>
        table div.ui-slider::before, div.ui-slider::after {
            display: block;
        }
    </style>

    <!-- Salesforce1 -->
    <apex:includeScript value="/canvas/sdk/js/publisher.js" />

    <!-- My own scripts -->
    <script>
        $j = jQuery.noConflict();
        
        // Process submit button clicked
        Sfdc.canvas.publisher.subscribe({name: "publisher.post", onData:function(e) {
             SubmitData();
        }});
        
        function SubmitData() {
            $j('#statusPopupID').popup('open');
            jsSubmitData();
        }
    
        $j(document).ready(function() {
            // Disable submit button
            Sfdc.canvas.publisher.publish({name: "publisher.setValidForSubmit", payload:"true"});
        });
    </script>
</head> 
<body>
<div data-role="page">
    <div data-role="content">
        <div data-role="popup" id="statusPopupID" class="ui-content" data-overlay-theme="a">
            Please Wait...<br/>
            <img src="{!$Resource.ProgressBar}"/>
        </div>
        <apex:form >
            <div data-role="collapsible" data-collapsed="false" data-content-theme="c">
                <h3><div style="white-space:normal">Search Engine</div></h3>
                <p>
                    <!-- Search engine -->
                    <Label for="{!$component.IsReimbursable}">Which expenses do you want to show?</Label>
                    <table width="100%">
                        <tr>
                            <td><label for="{!$component.IsReimbursable}">Only Reimbursable</label></td>
                            <td>
                                <apex:selectList value="{!soIsReimbursable}" id="IsReimbursable" html-data-role="slider" html-data-mini="true" >
                                    <apex:selectOptions value="{!soYesNo}" />
                                </apex:selectList>
                            </td>
                        </tr>
                        <tr>
                            <td><label for="{!$Component.IsUnreported}">Only Unreported</label></td>
                            <td>
                                <apex:selectList value="{!soIsUnreported}" id="IsUnreported" html-data-role="slider" html-data-mini="true" >
                                    <apex:selectOptions value="{!soYesNo}" />
                                </apex:selectList>
                            </td>
                        </tr>
                        <tr>
                            <td><label for="{!$Component.LocalAmount}">Matching $ Local:</label></td>
                            <td>
                                <apex:input type="number" html-step="0.01" html-name="LocalAmount" id="LocalAmount" html-data-mini="true" value="{!LocalAmount}" />
                            </td>
                        </tr>
                        <tr>
                            <td/>
                            <td>
                                <a href="javascript:SubmitData()" data-role="button" data-inline="true" data-mini="true" data-theme="b">Search</a>
                                <apex:actionFunction name="jsSubmitData" action="{!ApplyFilter}"/>
                            </td>
                        </tr>
                    </table>
                </p>
            </div>
            
            <!-- Show Data -->
            <table width="100%" cellspacing="0" border="0">
                <apex:repeat value="{!Expenses}" var="e">
                    <tr>
                        <td valign="top">
                            <a href="javascript:sforce.one.navigateToSObject('{!e.record.ID}');" >{!e.record.Name}</a>
                        </td>
                        <td valign="top">
                            <apex:outputText value="{0, date, EEE}">
                                <apex:Param value="{!e.record.PurchaseDate__c}" />
                            </apex:OutputText>
                            <apex:outputText value="{0, date, (MMM dd)}">
                                <apex:Param value="{!e.record.PurchaseDate__c}" />
                            </apex:OutputText>
                        </td>
                    </tr>
                    <tr>
                        <td valign="top">
                            {!e.record.Location__c}&nbsp;<apex:outputText value="+" rendered="{!e.hasCoordenates}" />
                        </td>
                        <td valign="top">
                            {!e.record.ExpenseType__r.Name}
                        </td>
                    </tr>
                    <tr>
                        <td valign="top">
                            {!e.record.PaymentType__c}
                        </td>
                        <td valign="top">
                            <apex:outputPanel rendered="{!e.record.AmountLocal__c > 0}">
                                {!e.record.Currency__c}&nbsp;<apex:outputField value="{!e.record.AmountLocal__c}" />
                                <apex:outputPanel rendered="{!e.isLocalAmount}">
                                    <br/>USD&nbsp;<apex:outputField value="{!e.record.AmountConcur_Workflow__c}" />
                                </apex:outputPanel>
                            </apex:outputPanel>
                        </td>
                    </tr>
                    <tr>
                        <td colspan="2" valign="top">
                            <apex:outputPanel style="white-space:nowrap">
                                <apex:outputField value="{!e.record.isReimbursable__c}"/>&nbsp;Reimbusable<br/>
                            </apex:outputPanel>
                            <apex:outputPanel style="white-space:nowrap">
                                <apex:outputField value="{!e.record.isAmex2Concur__c}"/>&nbsp;Amex2Concur<br/>
                            </apex:outputPanel>
                            <apex:outputPanel style="white-space:nowrap">
                                <apex:outputField value="{!e.record.isMe2Concur__c}" lang=""/>&nbsp;Me2Concur<br/>
                            </apex:outputPanel>
                        </td>
                    </tr>
                    <!-- Notes -->
                    <apex:outputPanel rendered="{!e.hasNotes}">
                        <tr>
                            <td colspan="2">{!e.record.Description__c}</td>
                        </tr>
                    </apex:outputPanel>
                    <tr>
                        <td colspan="2"><hr/></td>
                    </tr>
                </apex:repeat>
            </table>
        </apex:form>
Version #1.6<br/>
    </div><!-- /content -->
</div><!-- /page -->
</body>
</html>
</apex:page>

This is the controller for that page:

global with sharing class sf1Trip_ViewExpenses {
    private String TripID;
    private transient List<ExpenseData> transientExpenses;
    private transient List<SelectOption> transientYesNo;
    
    public Boolean soIsReimbursable { get; set; }
    public Boolean soIsUnreported { get; set; }
    public Decimal LocalAmount { get; set; }
    
    public List<SelectOption> soYesNo {
        get {
            if (transientYesNo == null) {
                transientYesNo = new List<SelectOption>();
                transientYesNo.add(new SelectOption('false', 'false'));
                transientYesNo.add(new SelectOption('true', 'true'));
            }
            return transientYesNo;   
        }
    }
    public List<ExpenseData> expenses {
        get {
            if (transientExpenses == null) {
                transientExpenses = findExpenses();
            }
            return transientExpenses;    
        }
    }
    
    public sf1Trip_ViewExpenses(ApexPages.StandardController controller) {
        TripID = controller.getId();
        soIsReimbursable = false;
        soIsUnreported = false;
    }
    
    public PageReference ApplyFilter() {
        transientExpenses = findExpenses();
        LocalAmount = null;
        return null;
    }

    private List<ExpenseData> findExpenses() {
        String SOQL;
        List<ExpenseData> output = new List<ExpenseData>();
        
        SOQL = '';
        SOQL += 'SELECT ID, Name, ';
        // Basic Data
        SOQL += 'Currency__c, PaymentType__c, AmountLocal__c, ExpenseType__c, PurchaseDate__c, Description__c, ';
        // Releated Trip data
        SOQL += 'Trip__c, Trip__r.Location__c, ExpenseType__r.Name, ';
        // Calculated Data
        SOQL += 'AmountConcur_Workflow__c, isActiveTrip__c, isMealExpense__c, ExpenseOrder__c, Report_Order__c, ';
        // Checkboxes
        SOQL += 'isReimbursable__c, isAmex2Concur__c, isMe2Concur__c, ';
        // Location
        SOQL += 'Location__c, GPS__Latitude__s, GPS__Longitude__s ';
        SOQL += 'FROM Expense__c ';
        SOQL += 'WHERE Trip__c = :TripID ';
        if (soIsReimbursable) {
            SOQL += 'AND isReimbursable__c = true ';
        }
        if (soIsUnreported) {
            SOQL += 'AND isMe2Concur__c = false ';
        }
        System.debug('LocalAmount: ' + LocalAmount);
        if (LocalAmount != null) {
            SOQL += 'AND AmountLocal__c = :LocalAmount ';
        }
        SOQL += 'ORDER BY Report_Order__c DESC';
        System.debug('SOQL: ' + SOQL);
        for (Expense__c expense : Database.query(SOQL)) {
            output.add(new ExpenseData(expense));
        }
        return output;
    }
    
    public class ExpenseData {
        public Expense__c record { get; private set; } 
        public Boolean hasNotes { get { return record.Description__c != null; } }
        public Boolean isLocalAmount { get { return record.Currency__c == 'Local'; } }
        public Boolean hasCoordenates { get { return (record.GPS__Latitude__s != null) && (record.GPS__Longitude__s != null); } }
        public String mapURL { get { return 'https://maps.google.com/?z=5&q=' + record.GPS__Latitude__s + ',' + record.GPS__Longitude__s; } }
        
        private ExpenseData() {}
        public ExpenseData(Expense__c record) {
            this.record = record;
        } 
    }
}

Expense Record Page

One last feature I would like to demonstrate in the application I built, is putting Visualforce pages inside the record details page. If the user submitted the geo-coordinates for the place of purchase, then when the expense is rendered, a map will the location.

How did I put this Visualforce page in the record details page?

There was no difference in adding this page to a mobile page layout, than it is to add it to a regular page layout… And that is exactly the beauty of Salesforce1… You already know how to make “mobile applications” if you only know how to make “desktop applications”.

How did I make this Visualforce page?

This is the code to show this map:

<apex:page StandardController="Expense__c" cache="true">
    <apex:outputPanel rendered="{!AND(Expense__c.GPS__Latitude__s!=null, Expense__c.GPS__Longitude__s!=null)}">
        <apex:image value="http://maps.googleapis.com/maps/api/staticmap?center={!Expense__c.GPS__Latitude__s},{!Expense__c.GPS__Longitude__s}&markers=color:red%7C{!Expense__c.GPS__Latitude__s},{!Expense__c.GPS__Longitude__s}&zoom=7&size=300x300&maptype=roadmap&sensor=true" /> 
    </apex:outputPanel>
    <apex:outputPanel rendered="{!OR(Expense__c.GPS__Latitude__s=null, Expense__c.GPS__Longitude__s=null)}">
        <b>GPS coordenates not found!</b>
    </apex:outputPanel>
</apex:page>

These were the lessons I learned, and that you should focus on:
 
1. Publisher Actions should
a. Have short descriptive names with action oriented words (“Add Expense”, “Make Active”, …)
b. Be short and simple interfaces, focus on Micro-moments
 
2. Visualforce Pages
a. Control the Viewstate, either by using Visualforce Remoting or by marking your variables as transient.
b. Do not use fixed size components, since you do not know which browser or device will use your page. Maybe a phone (short and tall) or a table (wide). So use responsive pages and CSS.
c. To increate performance of your Visualforce pages, make sure the pages are marked with <apex:page cache="true">
d. The pages must be checked as “Available for Salesforce mobile apps”
e. Optimize your pages for mobile, you can use common mobile toolkits like jQuery Mobile for example.
 
3. General Notes
a. You can control the order in which the objects appear in the “Recent” section of the navigation menu, by pinning the objects on the Global Search Results.
b. The SDK is still a good option if you do not want to use the Salesforce1 provided interface or if you want your community users to use the app.

Want more?

I knew you would... So I created an unmanaged package that you can install and play with. I made it unmanaged, so you can see the code and make changes to the Apex code. These are the steps to accomplish that:

1. Go to Developerforce.com and get a free Developer ORG or click here.
2. Install this unmanaged package (http://bit.ly/ETSF1) on the brand new ORG. The password is "salesforce1"

Feel free to experiment... and drop me a note below if you found this useful.

If you are at Dreamforce, make sure you pick up a copy of this book, after Dreamforce find the book here: http://bit.ly/1h3j9eo

Everything I learned to build this app was in this great book.

comments powered by Disqus

© El Toro . IT @ 2013
Andrés Pérez