Create a dashboard page (Blazor)

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 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.

  1. Open server\Pages\Home.razor.cs with Visual Studio. Radzen will not overwrite any changes made to this file so it is the ideal place to add custom code.
  2. Replace the existing content with the following:
    using System;
    using System.Linq;
    using Microsoft.AspNetCore.Components;
    using Microsoft.EntityFrameworkCore;
    using RadzenCrm.Data;
    
    namespace RadzenCrm.Pages
    {
       public partial class HomeComponent
       {
           [Inject]
           public CrmContext Context { get; set; }
       }
    }
    

    This code injects the CrmContext (Entity Framework context) which we will use to query the database.

  3. Add a new class Stats in the same file. It will contain the monthly stats we are interested in
    public 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; }
    }
    
  4. Add a new method to the HomeComponent class. It will 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.

  1. Open server\project.csproj with Vusual Studio (or the server directory with Visual Studio Code).
  2. Open server\Controllers\ServerMethodsController.cs.
  3. Replace the existing content with the following:
    using System;
    using System.IO;
    using System.Linq;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Serialization;
    using RadzenCrm.Data;
    
    namespace RadzenCrm.Controllers
    {
        [Route("api/[controller]/[action]")]
        public class ServerMethodsController : Controller
        {
            private readonly CrmContext context;
    
            public ServerMethodsController(CrmContext context)
            {
                this.context = context;
            }
        }
    }
    

    This code injects the CrmContext (Entity Framework context) which we will use to query the database.

  4. Add the following method to the ServerMethodsController class.
    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(JsonSerializer.Serialize(stats, new JsonSerializerOptions { PropertyNamingPolicy = null }));
    }
    
  5. Open client\Pages\Home.razor.cs with Visual Studio. Radzen will not overwrite any changes made to this file so it is the ideal place to add custom code.
  6. Replace the existing content with the following:
    using System;
    using System.Linq;
    using Microsoft.AspNetCore.Components;
    using Microsoft.EntityFrameworkCore;
    using RadzenCrm.Data;
    
    namespace RadzenCrm.Pages
    {
       public partial class HomeComponent
       {
          [Inject]
          HttpClient Http { get; set; }
       }
    }
    

    This code injects the HttpClient which we will use to query the server method.

  7. Add a new class Stats in the same file. It will contain the monthly stats we are interested in
    public 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; }
    }
    
  8. Add a new method to the HomeComponent class. It will calculate the stats.
    public async Task<Stats> MonthlyStats()
    {
       var response = await Http.SendAsync(new HttpRequestMessage(HttpMethod.Get, new Uri($"{UriHelper.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.

  1. Create a new empty page in Radzen called Home. Check Start page so this is what the user sees when he logs in the CRM application.
  2. Drag and drop a Row component.
  3. Drag and drop a Column component inside the row.
  4. 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.
  5. 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.

  1. Drag and drop a Row inside the card.
  2. 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).
  3. Duplicate the column. Set its XS size to 8. This column will occupy the remaining space.
  4. Drag and drop an Icon component in the first column. Set its Icon property to attach_money. Set the Width and Height to 64px. Set the BackgroundColor to Info (from the predefined colors). Set FontSize to 48px.
  5. Drag and drop a Heading component in the second column. Set its Text property to Revenue, Size to H4, TextAlign to Right and Margin to 0px.
  6. Duplicate the heading. Set Text to LAST MONTH, Color to Cool grey and FontSize to 12px.
  7. Duplicate the last heading. Set Text to Value, Color to Info, top Margin to 13px to offset it from the other headings a bit, and FontSize to 24px.

We have created the card design and it is time to display the Revenue in the last heading component that we added.

  1. Add a new handler for the Page Load event.
  2. Set its Type to Invoke method. Pick MonthlyStats from the dropdown.
  3. Handle the Then event and create a new page property. Set its Name to monthlyStats and Value to ${result}.
  4. Select the last heading component in the card. Set its Text property to ${monthlyStats?.Revenue.ToString("C")}. This displays the Revenue member of the monthlyStats 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

  1. Select the card component. Then click the ^ button which will select its parent component - the column.
  2. Duplicate the column.
  3. Select the Icon. Set its Icon property to shopping_cart. Set BackgroundColor to Danger.
  4. Select the first heading and set its Text to Opportunities.
  5. Select the bottom heading. Set Text to ${monthlyStats?.Opportunities.ToString()} and Color to Danger.

Average deal size

  1. Select the card component. Then click the ^ button which will select its parent component - the column.
  2. Duplicate the column.
  3. Select the Icon. Set its Icon property to account_balance_wallet. Set BackgroundColor to Primary.
  4. Select the first heading and set its Text to Average Deal Size.
  5. Select the bottom heading. Set Text to ${monthlyStats?.AverageDealSize.ToString()} and Color to Primary.

Win rate

  1. Select the card component. Then click the ^ button which will select its parent component - the column.
  2. Duplicate the column.
  3. Select the Icon. Set its Icon property to thumb up. Set BackgroundColor to Secondary.
  4. Select the first heading and set its Text to Win Rate.
  5. Select the bottom heading. Set Text to ${monthlyStats?.Ratio.ToString("P")} and Color to Secondary.

The final result should look like this at runtime.

Show charts

Get data for the charts

Server-side Blazor

  1. Open server\Pages\Home.razor.cs with Visual Studio. Radzen will not overwrite any changes made to this file so it is the ideal place to add custom 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

  1. Open server\project.csproj with Vusual Studio (or the server directory with Visual Studio Code).
  2. Open server\Controllers\ServerMethodsController.cs.
  3. 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(JsonSerializer.Serialize(result, new 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(JsonSerializer.Serialize(result, new 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(JsonSerializer.Serialize(result, new JsonSerializerOptions
       {
             PropertyNamingPolicy = null
       }));
    }
    
  4. Open client\Pages\Home.razor.cs with Visual Studio. Radzen will not overwrite any changes made to this file so it is the ideal place to add custom code.
    public async Task<IEnumerable<RevenueByCompany>> RevenueByCompany()
    {
       var response = await Http.SendAsync(new HttpRequestMessage(HttpMethod.Get, new Uri($"{UriHelper.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($"{UriHelper.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($"{UriHelper.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.

  1. Drag and drop a Row.
  2. 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 to 6 (1/2th of the available space).
  3. 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

  1. Drag and drop a Heading component inside the card. Set its Text to Customer life time value.
  2. Invoke the RevenueByCompany custom method in the Page Load event. Create a page property called revenueByCompany with the ${result}.
  3. Drag and drop a Chart and add Bar to series. Set Type to Column, Data to ${revenueByCompany}, ValueProperty to Revenue and CategoryProperty to Company. Set the Width to 100%.

Revenue by month chart

  1. Duplicate the column from the previous step.
  2. Set the Text of the header to Revenue.
  3. Invoke the RevenueByMonth custom method in the Page Load event. Create a page property called revenueByMonth with the ${result}.
  4. Set the Type of the chart to Area, Data to ${revenueByMonth}, and CategoryProperty to Month.

Revenue by employee chart

  1. Duplicate the column from the previous step.
  2. Set the Text of the header to Revenue by employee.
  3. Invoke the RevenueByEmployee custom method in the Page Load event. Create a page property called revenueByEmployee with the ${result}.
  4. Set the Type of the chart to Bar, Data to ${revenueByEmployee}, and CategoryProperty to Employee.

Here is how the end result should look like.

Summary of recent opportunities and tasks

As usual we start with the layout.

  1. Drag and drop a Row.
  2. Drop a Column inside. Set its XL size to 6.
  3. Drop a Card inside the column.
  4. Add a Heading inside the card.

Recent opportunities

First we need to get the data.

  1. Invoke the getOpportunities data source method in the page Load event.
  2. Open the query builder and sort by CloseDate in descending order.
  3. Handle the Then event and set a page property called getOpportunitiesResult with Value ${result} for server-side Blazor and ${result.Value.AsODataEnumerable()} for client-side WebAssembly Blazor. For client-side WebAssembly Blazor set alost $expand parameter to Contact,OpportunityStatus

Then we can add a DataGrid.

  1. Drag and drop a DataGrid component below the heading.
  2. Set Data to ${getOpportunitiesResult}. Check AllowSorting.
  3. Add a column and set its Template to ${data.Contact.FirstName} ${data.Contact.LastName}. Set the Title to Contact and SortProperty to Contact.FirstName.
  4. Add another column and set its Property to Amount. Set Template to ${data.Amount.ToString("C")}.
  5. Add a third column and set Property to OpportunityStatus.Name and Title to Status.
  6. Add the last column. Set Property to CloseDate.

Tasks

  1. Invoke the getTasks data source method in the page Load event.
  2. Open the query builder and sort by DueDate in descending order.
  3. Handle the Then event and set a page property called getTasksResult with Value ${result} for server-side Blazor and ${result.Value.AsODataEnumerable()} for client-side WebAssembly Blazor.

Server-side Blazor

We also need to include the User property in the result of the getTasks operation.

  1. Open the server\Services\CrmService.Custom.cs file.
  2. Add the following method to the CrmService class:
    partial void OnTasksRead(ref IQueryable<Task> items)
    {
       items = items.Include(item => item.Opportunity.User).Include(item => item.Opportunity.Contact);
    }
    

Client-side WebAssembly Blazor

Set alost $expand parameter to Opportunity($expand=User,Contact).

Then duplicate the other column that has the Opportunities DataGrid.

  1. Duplicate the column.
  2. Set the heading Text to Active Tasks.
  3. Delete the existing DataGrid and drag-and-drop a new one.
  4. Set the Data property of the DataGrid to ${getTasksResult}.
  5. Delete all columns and add a new one with Title Employee and SortProperty to Opportunity.User.FirstName. Edit its Template.
  6. Drag and drop an Image component. Set its Path to ${data.Opportunity.User.Picture}. Set Width and Height to 30px and BorderRadius to 15px.
  7. Drag and drop a Label component. Set its Text to ${data.Opportunity.User.FirstName}.
  8. Drag and drop another Label component. Set its Text to ${data.Opportunity.User.LastName}.
  9. End template editing and add another column with Property set to Title.
  10. Add another column. Set SortProperty to Opportunity.Name. Edit its Template.
  11. Drag and drop a Label component. Set its Text to ${data.Opportunity.Name}. Go to Style and set Display to block. This will make the next label appear on a new line.
  12. Drag and drop another Label component. Set its Text to ${data.Opportunity.Contact.FirstName}.
  13. Duplicate that label. Set the Text of the new one to ${data.Opportunity.Contact.LastName} and end template editing.
  14. 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.