<- All posts

How to Build a Sales Dashboard with 黑料正能量

Ronan McQuillan
18 min read Sep 19, 2023

Sales dashboards are fundamental to how we track, monitor, and plan around our incoming revenues. Businesses today sit on mountains of data. But, when it comes to sales reporting - decision-makers often only need high-level metrics.

So, the challenge is cutting through the noise and providing clear, actionable insights to the right people.

Today, we鈥檙e exploring how 黑料正能量 can be used to build complex, custom solutions with all sorts of pre-existing business data.

By the end, you鈥檒l have a full working knowledge of how to use our platform to turn data into action, based around your existing internal processes and data assets.

We鈥檒l also provide you with all of the necessary code snippets and queries you need to perform the actions carried out in this tutorial.

But first, a little bit of background.

What is a sales dashboard?

A sales dashboard is a simple UI for reporting data related to key sales metrics. The idea is that it can be preconfigured with set metrics that update in real time as new data is populated from an external source.

That way, users can simply navigate to our application each time they need to get an update on progress, rather than having to rebuild the same report over and over again.

At a technical level, dashboards are pretty simple solutions.

Essentially, we have a preexisting data source - or multiple data sources - containing information relevant to whichever KPIs we want to track.

We can then build user interfaces on top of this in order to visualize and communicate this data - with some combination of charts, cards, and tables.

Often, dashboards will be single-screen apps.

However, in practice, we鈥檒l often encounter some important complications here. Specifically, we鈥檒l usually need to perform aggregations, transformations, and other processes on our raw data in order to garner the insights that we actually need to present.

Since this can take up internal development time, many businesses are seeking out solutions that empower non-developers to build professional dashboards.

Let鈥檚 check out what we鈥檙e building in our tutorial in order to get a feel for what this looks like in the real world.

What are we building?

This tutorial is a follow-on from our previous guide to building complex approvals . In that, we created an application where regional sales teams can submit monthly figures on their performance.

A central team can then review and approve these. When they鈥檙e approved, they鈥檙e added to a Postgres database table called sales_kpi - which stores the month, year, region, and all of the sales metrics relating to the report.

So, we鈥檝e already connected our database to our 黑料正能量 app and created all of the screens and workflows we need around the report submission and approval process.

Today, we鈥檙e specifically thinking about how to build a sales dashboard based around our approved reports.

Data model

For the application as whole, the data model is a bit more complicated - but for our purposes in this guide, it鈥檚 quite simple. Our dashboard is only going to query a single table - storing all of the information around approved sales reports.

This includes the following attributes about each region鈥檚 reports:

  • The month.
  • The year.
  • The region.
  • The submitting manager.
  • The approver.
  • Status.
  • Total marketing costs.
  • Number of leads.
  • Number of customers.
  • Number of sales.
  • Revenue.

A new entry is added to this table when a regional manager submits a report and this gets approved by the central team.

Here鈥檚 what this table looks like in 黑料正能量:

Sales dashboard

UI

The UI is going to be the home screen for our existing app. Effectively, this has two sections:

  • A series of cards displaying total, current month, and previous month values for revenue, conversions, and cost of acquisition.
  • Two charts displaying total revenue over time and revenue growth by region over time.

So, here are the cards:

Sales Dashboard

And our charts:

Charts

How to build a sales dashboard in 5 steps

Okay, so now we have a pretty good idea of what we want to build. Of course, this is just an exemplary scenario. With 黑料正能量, you also have full control over how closely you want to follow our guide.

With that in mind, let鈥檚 check out how we鈥檝e built our sales dashboard.

1. Revenue cards

Following on from our previous channel, our first step is to create a blank screen, with 鈥/鈥 as its page path. This will be the app鈥檚 home screen, so we鈥檒l also add it to our nav bar as Home.

Blank screen

The cards section of our dashboard UI is made up of three sets of three cards. The first row that we鈥檙e going to build will display:

  1. The monthly revenue growth rate.
  2. This month鈥檚 total revenue.
  3. Last month鈥檚 total revenue.

Now, you might have spotted when we outlined the data stored in our table that we don鈥檛 actually have these specific pieces of information stored - at least, not explicitly.

So, we鈥檙e going to need to extract these insights from our data table by writing a custom query to our Postgres database. 黑料正能量 allows us to write, test, save, and reuse custom queries across our application - just like we鈥檇 use any other connected data source.

So, to start, head over to the data section of the 黑料正能量 builder, and hit create new query under your existing Postgres data source:

Create query

This will present us with a screen where we can configure and write our query - along with any transformations we want to apply to the returned data.

Config

This is going to be quite a complex query, so we鈥檙e going to take it step by step. We鈥檒l think about how to return each of the three values that we need in turn.

So first, the monthly revenue growth rate. Basically, we want to select this month鈥檚 total revenue figure minus last month鈥檚. We鈥檒l then divide the answer by last month鈥檚 revenue, and multiply this by 100.

We鈥檒l call this revenue_growth_rate. So, the first part of our SQL statement is:

1SELECT
2
3鈥	(this_month_revenue - last_month_revenue) / last_month_revenue * 100 
4
5AS revenue_growth_rate

But, this_month_revenue and last_month_revenue don鈥檛 actually exist in our database. We need to create those by summing the revenues that match a defined month and year.

To make this work dynamically, we鈥檒l need to create four bindings. These are variables that can be passed to our query from the UI or an automation each time it鈥檚 executed. Each binding has a name and a default value which can be set when we create the query.

We鈥檒l hit add bindings to create four bindings called this_month, this_year, last_month, and last_month_year. Note, that we also need to know what year it was last month, in case we want to use our dashboard in January.

We鈥檒l give each of these default values which relate to the current and previous month as they are now - in September 2023.

Bindings

So, now we can use handlebars syntax to leverage these bindings in our query. We want to add clauses to sum the revenues where the year and month match our bindings for the current month - and then do the same for the previous month - using WHERE conditions.

We鈥檒l call the outputs this_month_revenue and last_month_revenue using AS statements.

When we add these lines on, our query will be:

 1SELECT
 2
 3鈥	(this_month_revenue - last_month_revenue) / last_month_revenue * 100 
 4
 5AS revenue_growth_rate
 6
 7FROM
 8
 9 (SELECT SUM(revenue) AS this_month_revenue FROM sales_kpi WHERE month = {{ this_month}} AND year = {{ this_year}}) AS this_month_revenue,
10
11鈥	(SELECT SUM(revenue) AS last_month_revenue FROM sales_kpi WHERE month = {{ last_month }} AND year = {{ last_month_year }}) AS last_month_revenue

This returns our monthly growth rate, but we haven鈥檛 specified any rounding, so we get quite a few decimal places - depending on the real figures being processed:

Sales dashboard

So, we鈥檒l use the ROUND function to reduced our data down to two decimal places:

 1SELECT
 2
 3鈥	ROUND((this_month_revenue - last_month_revenue) / last_month_revenue * 100, 2) 
 4
 5AS revenue_growth_rate
 6
 7FROM
 8
 9 (SELECT SUM(revenue) AS this_month_revenue FROM sales_kpi WHERE month = {{ this_month}} AND year = {{ this_year}}) AS this_month_revenue,
10
11鈥	(SELECT SUM(revenue) AS last_month_revenue FROM sales_kpi WHERE month = {{ last_month }} AND year = {{ last_month_year }}) AS last_month_revenue

We鈥檙e going to use rounding every time we divide or multiply in our custom queries from now on - but we鈥檙e not going to draw attention to it every time.

result

We鈥檒l save this query as MonthlyRevenue.

While we鈥檙e here, we also want to return this_month_revenue and last_month_revenue in the response from this same query.

We can simply add these to our existing select statement, making our final query:

 1SELECT
 2
 3鈥	ROUND((this_month_revenue - last_month_revenue) / last_month_revenue * 100, 2) AS revenue_growth_rate,
 4
 5鈥	this_month_revenue,
 6
 7鈥	last_month_revenue
 8
 910
11FROM
12
13 (SELECT SUM(revenue) AS this_month_revenue FROM sales_kpi WHERE month = {{ this_month}} AND year = {{ this_year}}) AS this_month_revenue,
14
15鈥	(SELECT SUM(revenue) AS last_month_revenue FROM sales_kpi WHERE month = {{ last_month }} AND year = {{ last_month_year }}) AS last_month_revenue

Custom Query

And the last thing we鈥檒l do is specify that each of these values are numbers under the schema tab:

Schema

That鈥檚 the hard part over for this set of cards.

Next, let鈥檚 put our query to use. Head over to the design tab in 黑料正能量. On our blank screen, we鈥檙e going to start by adding a headline component with its text set to Revenue, followed by a horizontal container component.

Container

Eventually, we鈥檒l have three cards inside this component, but we鈥檙e going to start by adding just one - which we鈥檒l configure and then duplicate.

So, add a card block and set its data source to our monthlyRevenue query:

Card

We鈥檒l give it a title of Revenue Growth and remove its description entirely. Then, beside the subtitle field, we鈥檒l hit the lightning bolt to open our bindings menu and select the revenue_growth_rate attribute:

Populate data

This looks right, but its not actually going to function correctly just yet. See, we鈥檙e still relying on the query bindings鈥 default values. We need to tell our card block to override these with the actual values for this month and last month to get the real-time calculation.

Hit the cog icon beside the data field to access a bindings menu for our query:

Query bindings

We鈥檙e going to use a mixture of handlebar expressions and custom JavaScript as bindings to populate the actual months and years we need.

For this_month we can use handlebars to get the current month in a numerical format with the expression:

1{{ date now "M" }}

And do the same thing for the year using:

1{{ date now "YYYY" }}

That鈥檚 easy enough. But, if we just did this using handlebars for last month鈥檚 values, we鈥檇 encounter issues around the start and end of the year, so we鈥檙e going to use custom JavaScript.

So, for last_month, our binding will be:

1let today = new Date()
2
3today.setMonth(today.getMonth() - 1)
4
5let previousMonth = today.getMonth() + 1
6
7return previousMonth

For last_month_year, we can use:

 1let today = new Date()
 2
 3today.setMonth(today.getMonth() - 1)
 4
 5if(today.getMonth() === 11) {
 6
 7 today.setFullYear(today.getFullYear() - 1)
 8
 9}
10
11let previousMonthYear = today.getFullYear()
12
13return previousMonthYear

This if statement is checking if last month was December. If so, we鈥檙e subtracting one from the year.

Now, our card will function as we need it to. The last thing we鈥檒l do is set its width to 30% so it takes up just under a third of the width of the screen:

Width

To save us configuring everything over again for our next two cards, we鈥檒l just duplicate this existing one twice. We can then swap out the titles and subtitle bindings to reflect our other two metrics, giving us:

Cards row

Join 200,000 teams building workflow apps with 黑料正能量

2. Conversions cards

Next, we鈥檒l repeat a similar process to build our next set of cards. That is, we鈥檒l need to write a query to get the values we want and then leverage this in our UI.

So, head back to the data section. Create another new query called SalesConversion and add the exact same four bindable values.

Again, we want to return three values:

  1. Our overall conversion rate.
  2. This month鈥檚 conversion rate.
  3. Last month鈥檚 conversion rate.

We鈥檙e operationalizing the conversion rate as the number of sales divided by the number of leads for a given period - multiplied by 100.

Again, we鈥檒l run through this step-by-step, but this query is actually quite a bit more complex, so pay close attention.

We鈥檒l start by getting our overall conversion rate - since this is the easy part.

To get just this, the Postgres query would be:

1SELECT
2
3 ROUND(SUM(num_sales)*1.0/SUM(num_leads) * 100 ,2) as sales_conversion_overall
4
5FROM
6
7 sales_kpi

Query

Note, we are multiplying the number of sales by 1.0 to ensure that our SQL engine knows that we鈥檙e working in decimals rather than integers.

Next, we need our dynamic data for this month and last month鈥檚 conversion rates. We鈥檝e already created the exact same bindings as we had in our previous query.

The way we鈥檒l calculate sales_conversion_last_month is to use CASE statements to iterate over each row that has a month and year value that match the relevant bindings, and then sum together all of the leads and customers that match this condition.

We鈥檒l then perform the same calculation on these as before to reach our monthly conversion rate.

So, for last month, we鈥檙e selecting the following data from our sales_KPI table:

1ROUND( 
2
3SUM(CASE WHEN month = {{ last_month}} AND year = {{ last_month_year}} THEN num_sales ELSE 0 END) * 1.0 / 
4
5SUM(CASE WHEN month = {{ last_month}} AND year = {{ last_month_year}} THEN num_leads ELSE 0 END) * 100
6
78
9鈥		, 2) AS sales_conversion_last_month

And for sales_conversion_this_month, it鈥檚:

1鈥		ROUND( 
2
3SUM(CASE WHEN month = {{ this_month}} AND year = {{ this_year}} THEN num_sales ELSE 0 END) * 1.0 / 
4
5SUM(CASE WHEN month = {{ this_month }} AND year = {{ this_year}} THEN num_leads ELSE 0 END) * 100
6
78
9鈥		, 2) AS sales_conversion_this_month

That means that our full query is:

 1SELECT
 2
 3 ROUND(SUM(num_sales)*1.0/SUM(num_leads) * 100 ,2) as sales_conversion_overall,
 4
 5鈥	ROUND( 
 6
 7SUM(CASE WHEN month = {{ last_month}} AND year = {{ last_month_year}} THEN num_sales ELSE 0 END) * 1.0 / 
 8
 9SUM(CASE WHEN month = {{ last_month}} AND year = {{ last_month_year}} THEN num_leads ELSE 0 END) * 100
10
1112
13鈥		, 2) AS sales_conversion_last_month,
14
15鈥		ROUND( 
16
17SUM(CASE WHEN month = {{ this_month}} AND year = {{ this_year}} THEN num_sales ELSE 0 END) * 1.0 / 
18
19SUM(CASE WHEN month = {{ this_month }} AND year = {{ this_year}} THEN num_leads ELSE 0 END) * 100
20
2122
23鈥		, 2) AS sales_conversion_this_month
24
2526
27FROM
28
29 sales_kpi

Again, we鈥檒l specify that each of these are numbers, and we鈥檙e ready to go.

Sales Dashboard

Back to the design tab.

We could build another set of cards from scratch if we needed to, but we can save ourselves a bit of time by simply duplicating what we have so far.

Cards

Now, we need to do a few things here to update our new cards. Specifically, we want to:

  • Update the new headline to say Sales Conversion.
  • Change the data for each of our new cards to our SalesConversions query.
  • Reset our bindings to override the default values for our query - because these get deleted when we change the data.
  • Update the titles and subtitles of our cards for overall, this month鈥檚, last month鈥檚 conversion rates.
  • Give our components new names so things don鈥檛 get confusing.

Once we do this, we鈥檒l have:

Cards

3. Acquisition costs cards

Our last set of cards is going to display data relating to the cost of acquiring customers - again, broken down by overall, this month, and last month.

Head back to the data tab and create one more query - again with the same bindings. We鈥檒l call this one CostOfCustomerAcquisition.

This time, we鈥檙e operationalizing our cost of acquisition as the SUM of the total marketing costs divided by the SUM of the number of new customers for each of the respective periods.

The actual query this time is pretty similar to the last one, so we鈥檒l just give you the whole thing:

 1SELECT
 2
 3  ROUND(SUM(total_marketing_costs)*1.0 / SUM(num_new_customers), 2) AS cost_of_customer_over_time,
 4
 5鈥	 ROUND(
 6
 7SUM(CASE WHEN month = {{this_month}} AND year = {{ this_year}} THEN total_marketing_costs ELSE 0 END) * 1.0 / SUM(CASE WHEN month = {{this_month}} AND year = {{ this_year}} THEN num_new_customers ELSE 0 END)
 8
 9鈥	 , 2) AS cost_of_customer_this_month,
10
11鈥	 	 ROUND(
12
13SUM(CASE WHEN month = {{last_month}} AND year = {{ last_month_year}} THEN total_marketing_costs ELSE 0 END) * 1.0 / SUM(CASE WHEN month = {{last_month}} AND year = {{ last_month_year}} THEN num_new_customers ELSE 0 END)
14
15鈥	 , 2) AS cost_of_customer_last_month
16
1718
19FROM 
20
21鈥	sales_kpi

Sales Dashboard

Save that and head back to our UI section.

We鈥檒l repeat the same process of duplicating and modifying one of our existing rows of cards to display our acquisition cost data.

This gives us:

Cards

4. Monthly revenue chart

Okay, now all of our cards are built. Time to start creating some charts. The first one we want is our monthly revenue. We鈥檒l display this as a bar graph.

This one is pretty simple. All we need to do is get the total amount of money that we made in each month.

Head to the data section and create a query called TotalRevenueOverTime. We鈥檙e going to start by SELECTing the month, year, and the sum of revenues from our sales_kpi table. We鈥檒l group these by month and year.

We鈥檒l also ORDER by year ascending followed by month ascending.

So, our query is:

 1SELECT
 2
 3  ROUND(SUM(total_marketing_costs)*1.0 / SUM(num_new_customers), 2) AS cost_of_customer_over_time,
 4
 5鈥	 ROUND(
 6
 7SUM(CASE WHEN month = {{this_month}} AND year = {{ this_year}} THEN total_marketing_costs ELSE 0 END) * 1.0 / SUM(CASE WHEN month = {{this_month}} AND year = {{ this_year}} THEN num_new_customers ELSE 0 END)
 8
 9鈥	, 2) AS cost_of_customer_this_month,
10
11鈥	 	 ROUND(
12
13SUM(CASE WHEN month = {{last_month}} AND year = {{ last_month_year}} THEN total_marketing_costs ELSE 0 END) * 1.0 / SUM(CASE WHEN month = {{last_month}} AND year = {{ last_month_year}} THEN num_new_customers ELSE 0 END)
14
15鈥	 , 2) AS cost_of_customer_last_month
16
1718
19FROM 
20
21鈥	sales_kpi

We鈥檒l also tell 黑料正能量 that these are numbers again.

Monthly revenue query

Now we have a nice neat table of monthly revenue figures.

Back on our UI, we鈥檒l add a chart block below our last set of cards. We鈥檒l give it a name, set its type to bar, and its data to TotalRevenueOverTime. Then we鈥檒l select month as the label column and total_revenue as the data column:

Chart config

We鈥檒l also add a title so that it鈥檚 clear what our chart means:

Sales dashboard

5. Regional monthly revenue chart

Our regional monthly revenue chart is a bit more complex. What we want this time is a line graph that will display the trend of revenues for each of the regions that have submitted sales figures.

So, we can start with a similar query, except this time we鈥檒l also need to SELECT, GROUP BY, and ORDER using the region attribute.

This looks like this:

 1SELECT
 2
 3month,
 4
 5year,
 6
 7鈥	region,
 8
 9SUM(revenue) AS regional_revenue
10
11FROM
12
13鈥	sales_kpi
14
15GROUP BY
16
17 month, year, region
18
19ORDER BY
20
21 year ASC, month ASC, region ASC

We鈥檒l call this query RevenueByRegion. But, look at the table that it returns:

Revenue query

We have multiple rows for the same months, so this isn鈥檛 going to look right on a graph. Instead, we want one row per month.

In other words, our query should return a series of arrays in the format:

1month: 鈥渕尘/yyyy鈥, region: value, region: value, etc

To do this, we鈥檙e going to write a little bit of custom JavaScript in our transformer box. Specifically, we鈥檙e going to use the data.reduce() and Object.values() methods to create an accumulator and populate the relevant data into objects for each unique month.

Here鈥檚 our code, with comments explaining what each part does:

 1const transformedResults = data.reduce((acc, curr) => {
 2
 3 // Create a unique key for each month-year combination
 4
 5 const monthYearKey = `${curr.month}-${curr.year}`;
 6
 7 // Check if this month-year is already in the accumulator
 8
 9 if (!acc[monthYearKey]) {
10
11  acc[monthYearKey] = {
12
13   month: `${curr.month}/${curr.year}`,
14
15  };
16
17 }
18
19 // Add the regional_revenue under the region key for the specific month/year
20
21 acc[monthYearKey][curr.region] = curr.regional_revenue;
22
23 return acc;
24
25}, {});
26
27// Convert the transformed results object into an array
28
29const transformedResultsArray = Object.values(transformedResults);
30
31return transformedResultsArray

Now when we run our query, the returned data looks like this:

Transformation

In other words, we have one attribute to show the month, and then each region has it鈥檚 own attribute that stores its respective revenue figure for that month.

Back to the data section.

Let鈥檚 add another chart block. This time we鈥檒l call it RegionalRevenue, set its data to our new query, and choose a line graph for its type. We鈥檒l select month as the label column again, but this time we鈥檒l use each of our regions as data columns.

We鈥檒l also select the legend tick box to make it easier to read and then give it a title.

It should look like this:

Sales dashboard

And that鈥檚 our dashboard completed!

Of course, there鈥檚 near infinite scope for how you could adapt, modify, and customize our example. 黑料正能量 offers a huge range of data sources, custom queries, advanced transformations, and much much more.

To learn more about how we empower teams to turn data into action, check out our features overview .