Create a dashboard page
In this tutorial we will create a nice dashboard page for our CRM application that shows some performance metrics.
- Display the monthly stats - total revenue, new opportunities, average deal size and win rate percentage.
- Show some charts - contact life time value, revenue progress, revenue per employee.
- Summarize the active opportunities and tasks.
Before we start we would need more data in the CRM application. Run it from Radzen Blazor Studio and add more opportunities, contacts and tasks. Or you can use this SQL script which comes with sample data (but will delete all existing data).
Create the dashboard page
First create a new empty page called “Home”. Set it to be the start page of the application.
Display monthly stats
Calculate stats
Server-side Blazor
We will create a method which will calculate the monthly stats.
- Open
Pages\Index.razor.cs
and inject the Entity Framework context as a property:... [Inject] public Data.RadzenCRMContext Context { get; set; } ...
- Add a new class
Stats
in the same file. It will contain the monthly stats we are interested inpublic class Stats { public DateTime Month { get; set; } public decimal Revenue { get; set; } public int Opportunities { get; set; } public decimal AverageDealSize { get; set; } public double Ratio { get; set; } }
- Add a new method to the Index class to calculate the stats.
public Stats MonthlyStats() { double wonOpportunities = Context.Opportunities .Include(opportunity => opportunity.OpportunityStatus) .Where(opportunity => opportunity.OpportunityStatus.Name == "Won") .Count(); var totalOpportunities = Context.Opportunities.Count(); var ratio = wonOpportunities / totalOpportunities; return Context.Opportunities .Include(opportunity => opportunity.OpportunityStatus) .Where(opportunity => opportunity.OpportunityStatus.Name == "Won") .ToList() .GroupBy(opportunity => new DateTime(opportunity.CloseDate.Year, opportunity.CloseDate.Month, 1)) .Select(group => new Stats() { Month = group.Key, Revenue = group.Sum(opportunity => opportunity.Amount), Opportunities = group.Count(), AverageDealSize = group.Average(opportunity => opportunity.Amount), Ratio = ratio }) .OrderBy(deals => deals.Month) .LastOrDefault(); }
Client-side WebAssembly Blazor
We will create a method which will calculate the monthly stats.
- Add
Server\Controllers\ServerMethodsController.cs
with the following code.using System; using System.Net; using System.Data; using System.Linq; using Microsoft.Data.SqlClient; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; using Microsoft.AspNetCore.OData.Results; using Microsoft.AspNetCore.OData.Deltas; using Microsoft.AspNetCore.OData.Formatter; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Authorization; using CRMBlazorWasmRBS.Server.Models; using CRMBlazorWasmRBS.Server.Models.RadzenCRM; namespace CRMBlazorWasmRBS.Server.Controllers { [Route("api/[controller]/[action]")] public class ServerMethodsController : Controller { private CRMBlazorWasmRBS.Server.Data.RadzenCRMContext context; public ServerMethodsController(CRMBlazorWasmRBS.Server.Data.RadzenCRMContext context) { this.context = context; } public IActionResult MonthlyStats() { double wonOpportunities = context.Opportunities .Include(opportunity => opportunity.OpportunityStatus) .Where(opportunity => opportunity.OpportunityStatus.Name == "Won") .Count(); var totalOpportunities = context.Opportunities.Count(); var ratio = wonOpportunities / totalOpportunities; var stats = context.Opportunities .Include(opportunity => opportunity.OpportunityStatus) .Where(opportunity => opportunity.OpportunityStatus.Name == "Won") .ToList() .GroupBy(opportunity => new DateTime(opportunity.CloseDate.Year, opportunity.CloseDate.Month, 1)) .Select(group => new { Month = group.Key, Revenue = group.Sum(opportunity => opportunity.Amount), Opportunities = group.Count(), AverageDealSize = group.Average(opportunity => opportunity.Amount), Ratio = ratio }) .OrderBy(deals => deals.Month) .LastOrDefault(); return Ok(System.Text.Json.JsonSerializer.Serialize(stats, new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = null })); } }
- Open
Client\Pages\Index.razor.cs
and inject the HttpClient as a property:... [Inject] HttpClient Http { get; set; } ...
- Add a new class
Stats
in the same file. It will contain the monthly stats we are interested inpublic class Stats { public DateTime Month { get; set; } public decimal Revenue { get; set; } public int Opportunities { get; set; } public decimal AverageDealSize { get; set; } public double Ratio { get; set; } }
- Add a new method to the
Index
class. It will calculate the stats.public async Task<Stats> MonthlyStats() { var response = await Http.SendAsync(new HttpRequestMessage(HttpMethod.Get, new Uri($"{NavigationManager.BaseUri}api/servermethods/monthlystats"))); return await response.ReadAsync<Stats>(); }
Display the monthly stats
Now that the custom method is done it is time to create the UI that will display the stats. We will use a Row component with four Columns. Each column will contain a Card that displays a metric from the monthly stats.
We will describe the process to create one card and then copy paste it.
- Open Index page and delete the top most Column.
- Drag and drop a new Column component inside the row.
- Set the XL size to 3 units and the LG size to 6 units. By default the available horizontal space is 12 units. This means that on XL size devices (wide-screen desktops) this column will occupy 1/4th of the available width (because 3/12 = 1/4). On LG screens (laptop displays) it will occupy half the available space (6 / 12 = 1/2). On smaller screen sizes this column will take all the available width.
- Drag and drop a Card component inside the column. Go to the Style tab of the property grid and set its bottom Margin to
16px
.
We have created the basic card layout. Now let’s add some content that will display the data.
- Drag and drop a Row inside the card.
- Drag and drop a Column inside that row. Set its XS size to 4. This means that it will always occupy 4 units (1/3) of the available width (which in this case is the width of the card).
- Duplicate the column. Set its XS size to 8. This column will occupy the remaining space.
- Drag and drop an Icon component in the first column. Set its Icon property to
attach_money
. Set the Width and Height to64px
. Set FontSize to48px
. - Drag and drop a Heading component in the second column. Set its Text property to
Revenue
, Size toH4
, TextAlign toRight
and Margin to0px
. - Duplicate the heading. Set Text to
LAST MONTH
and FontSize to12px
. - Duplicate the last heading. Set Text to
Value
, top Margin to13px
to offset it from the other headings a bit, and FontSize to24px
.
We have created the card design and it is time to display the Revenue
in the last heading component that we added.
- Add a new override for the Page OnInitialized() method.
- Add new Invoke.
- Set Invoke method to
MonthlyStats
and set the result tomonthlyStats
public page property. - Select the last heading component in the card. Set its Text property to
@monthlyStats?.Revenue.ToString("C")
. This displays theRevenue
member of themonthlyStats
page property formatted as currency.
If you now run the application you should see the following (assuming you have used the sample SQL data linked before).
To display the rest of the monthly stats we should duplicate the column, change the color, heading texts and the monthlyStats
member they display.
Here is how:
Opportunities
- Duplicate the column.
- Select the Icon. Set its Icon property to
shopping_cart
. - Select the first heading and set its Text to
Opportunities
. - Select the bottom heading. Set Text to
@(monthlyStats?.Opportunities.ToString("C"))
.
Average deal size
- Duplicate the column.
- Select the Icon. Set its Icon property to
account_balance_wallet
. - Select the first heading and set its Text to
Average Deal Size
. - Select the bottom heading. Set Text to
@(monthlyStats?.AverageDealSize.ToString("C"))
.
Win rate
- Duplicate the column.
- Select the Icon. Set its Icon property to
thumb_up
. - Select the first heading and set its Text to
Win Rate
. - Select the bottom heading. Set Text to
@(monthlyStats?.Ratio.ToString("P"))
.
The final result should look like this at runtime.
Show charts
Get data for the charts
Server-side Blazor
- Open
Pages\Index.razor.cs
and add the following code:public class RevenueByCompany { public string Company { get; set; } public decimal Revenue { get; set; } } public class RevenueByEmployee { public string Employee { get; set; } public decimal Revenue { get; set; } } public class RevenueByMonth { public DateTime Month { get; set; } public decimal Revenue { get; set; } } public IEnumerable<RevenueByCompany> RevenueByCompany() { return Context.Opportunities .Include(opportunity => opportunity.Contact) .ToList() .GroupBy(opportunity => opportunity.Contact.Company) .Select(group => new RevenueByCompany() { Company = group.Key, Revenue = group.Sum(opportunity => opportunity.Amount) }); } public IEnumerable<RevenueByEmployee> RevenueByEmployee() { return Context.Opportunities .Include(opportunity => opportunity.User) .ToList() .GroupBy(opportunity => $"{opportunity.User.FirstName} {opportunity.User.LastName}") .Select(group => new RevenueByEmployee() { Employee = group.Key, Revenue = group.Sum(opportunity => opportunity.Amount) }); } public IEnumerable<RevenueByMonth> RevenueByMonth() { return Context.Opportunities .Include(opportunity => opportunity.OpportunityStatus) .Where(opportunity => opportunity.OpportunityStatus.Name == "Won") .ToList() .GroupBy(opportunity => new DateTime(opportunity.CloseDate.Year, opportunity.CloseDate.Month, 1)) .Select(group => new RevenueByMonth() { Revenue = group.Sum(opportunity => opportunity.Amount), Month = group.Key }) .OrderBy(deals => deals.Month); }
Client-side WebAssembly Blazor
- Open
Server\Controllers\ServerMethodsController.cs
. - Add the following methods.
public IActionResult RevenueByCompany() { var result = context.Opportunities .Include(opportunity => opportunity.Contact) .ToList() .GroupBy(opportunity => opportunity.Contact.Company) .Select(group => new { Company = group.Key, Revenue = group.Sum(opportunity => opportunity.Amount) }); return Ok(System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = null })); } public IActionResult RevenueByEmployee() { var result = context.Opportunities .Include(opportunity => opportunity.User) .ToList() .GroupBy(opportunity => $"{opportunity.User.FirstName} {opportunity.User.LastName}") .Select(group => new { Employee = group.Key, Revenue = group.Sum(opportunity => opportunity.Amount) }); return Ok(System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = null })); } public IActionResult RevenueByMonth() { var result = context.Opportunities .Include(opportunity => opportunity.OpportunityStatus) .Where(opportunity => opportunity.OpportunityStatus.Name == "Won") .ToList() .GroupBy(opportunity => new DateTime(opportunity.CloseDate.Year, opportunity.CloseDate.Month, 1)) .Select(group => new { Revenue = group.Sum(opportunity => opportunity.Amount), Month = group.Key }) .OrderBy(deals => deals.Month); return Ok(System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = null })); }
- Open
Client\Pages\Index.razor.cs
and add the following code:public async Task<IEnumerable<RevenueByCompany>> RevenueByCompany() { var response = await Http.SendAsync(new HttpRequestMessage(HttpMethod.Get, new Uri($"{NavigationManager.BaseUri}api/servermethods/RevenueByCompany"))); return await response.ReadAsync<IEnumerable<RevenueByCompany>>(); } public async Task<IEnumerable<RevenueByEmployee>> RevenueByEmployee() { var response = await Http.SendAsync(new HttpRequestMessage(HttpMethod.Get, new Uri($"{NavigationManager.BaseUri}api/servermethods/RevenueByEmployee"))); return await response.ReadAsync<IEnumerable<RevenueByEmployee>>(); } public async Task<IEnumerable<RevenueByMonth>> RevenueByMonth() { var response = await Http.SendAsync(new HttpRequestMessage(HttpMethod.Get, new Uri($"{NavigationManager.BaseUri}api/servermethods/RevenueByMonth"))); return await response.ReadAsync<IEnumerable<RevenueByMonth>>(); }
Add the charts
As in the previous step we will start with the layout - row with three columns.
- Drag and drop a Row.
- Drag and drop a Column inside the row. Set the XL size to
4
units (1/3th of the available space) and the LG size to6
(1/2th of the available space). - Drag and drop a Card inside the column. Go to the Style tab of the property grid and set its bottom Margin to
16px
.
Customer life time value chart
- Drag and drop a Heading component inside the card. Set its Text to
Customer life time value
. - Invoke the
RevenueByCompany
custom method in the Page OnInitialized() method. Create a page property calledrevenueByCompany
from the result. - Drag and drop a Chart and add RadzenColumnSeries to series. Set Data to
@revenueByCompany
, ValueProperty toRevenue
and CategoryProperty toCompany
. Set the Width to100%
.
Revenue by month chart
- Duplicate the column from the previous step.
- Set the Text of the header to
Revenue
. - Invoke the
RevenueByMonth
custom method in the Page OnInitialized() method. Create a page property calledrevenueByMonth
from the result. - Change RadzenColumnSeries to RadzenAreaSeries, set Data to
@revenueByMonth
, and CategoryProperty toMonth
.
Revenue by employee chart
- Duplicate the column from the previous step.
- Set the Text of the header to
Revenue by employee
. - Invoke the
RevenueByEmployee
custom method in the Page OnInitialized() method. Create a page property calledrevenueByEmployee
from the result. - Change RadzenColumnSeries to RadzenBarSeries, set Data to
@revenueByEmployee
, and CategoryProperty toEmployee
.
Here is how the end result should look like.
Summary of recent opportunities and tasks
As usual we start with the layout.
- Drag and drop a Row.
- Drop a Column inside. Set its XL size to
6
. - Drop a Card inside the column.
- Add a Heading inside the card.
Recent opportunities
- Add new injected Page property for
RadzenCRMService
and changeOnInitialized()
to be asynchronous. - Drag and drop a DataGrid component below the heading.
- Use DataGrid configuration wizard to bind the component to result of
GetOpportunities
data source method. Do not select any columns. - Select
Home.razor.cs
, choose methods & design, select methodOnInitializedAsync
, choose Query Builder from the properties panel and addContact,OpportunityStatus
asexpand
parameter. - Add a column and add to its Template tow Label components with Text set to
@context.Contact.FirstName
and@context.Contact.LastName
. Set the Title toContact
and SortProperty toContact.FirstName
. - Add another column and set its Property to
Amount
. Set Template to@context.Amount.ToString("C")
. - Add a third column and set Property to
OpportunityStatus.Name
and Title toStatus
. - Add the last column. Set Property to
CloseDate
.
Tasks
- Duplicate the column.
- Set the heading Text to
Active Tasks
. - Drag and drop new DataGrid component in the new column below the opportunities DataGrid.
- Use DataGrid configuration wizard to bind the component to result of
GetTasks
data source method. Do not select any columns.
Server-side Blazor
We also need to include the User
property in the result of the GetTasks
operation.
- Open the
Services\RadzenCRMService.Custom.cs
file. - Add the following method to the
RadzenCRMService
class:partial void OnTasksRead(ref IQueryable<CRMBlazorServerRBS.Models.RadzenCRM.Task> items) { items = items.Include(item => item.Opportunity.User).Include(item => item.Opportunity.Contact); }
Client-side WebAssembly Blazor
Set also expand
parameter to Opportunity($expand=User,Contact)
.
- Add a new DataGrid column with Title
Employee
and SortProperty toOpportunity.User.FirstName
. Edit its Template. - Drag and drop an Image component. Set its Path to
@context.Opportunity.User.Picture
. Set Width and Height to30px
and BorderRadius to15px
. - Drag and drop a Label component. Set its Text to
@context.Opportunity.User.FirstName
. - Drag and drop another Label component. Set its Text to
@context.Opportunity.User.LastName
. - End template editing and add another column with Property set to
Title
. - Add another column. Set SortProperty to
Opportunity.Name
. Edit its Template. - Drag and drop a Label component. Set its Text to
@context.Opportunity.Name
. Go to Style and set Display toblock
. This will make the next label appear on a new line. - Drag and drop another Label component. Set its Text to
@context.Opportunity.Contact.FirstName
. - Duplicate that label. Set the Text of the new one to
@context.Opportunity.Contact.LastName
and end template editing. - Add one last column with Property set to
DueDate
.
The Dashboard is now complete! Here is how it will look at runtime.
It is time for the finishing touches.