Select theme:
In this tutorial we will create a nice dashboard page for our CRM application that shows some performance metrics.
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).
First create a new empty page called "Home". Set it to be the start page of the application.
We will create a method which will calculate the monthly stats.
Pages\Index.razor.cs
and inject the Entity Framework context as a property:...
[Inject]
public Data.RadzenCRMContext Context { get; set; }
...
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; }
}
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();
}
We will create a method which will calculate the monthly stats.
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 }));
}
}
Client\Pages\Index.razor.cs
and inject the HttpClient as a property:...
[Inject]
HttpClient Http { get; set; }
...
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; }
}
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>();
}
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.
16px
.
We have created the basic card layout. Now let's add some content that will display the data.
attach_money
.
Set the Width and Height to 64px
. Set FontSize to 48px
. Revenue
,
Size to H4
, TextAlign to Right
and Margin to 0px
. LAST MONTH
and FontSize to 12px
. Value
, 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.
MonthlyStats
and set the result to monthlyStats
public page property. @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:
shopping_cart
.Opportunities
.@(monthlyStats?.Opportunities.ToString("C"))
.account_balance_wallet
.Average Deal Size
.@(monthlyStats?.AverageDealSize.ToString("C"))
.thumb_up
.Win Rate
.@(monthlyStats?.Ratio.ToString("P"))
.The final result should look like this at runtime.
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);
}
Server\Controllers\ServerMethodsController.cs
.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
}));
}
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>>();
}
As in the previous step we will start with the layout - row with three columns.
4
units (1/3th of the available space) and the LG size to 6
(1/2th of the available space).16px
.
Customer life time value
.RevenueByCompany
custom method in the Page OnInitialized() method. Create a page property called revenueByCompany
from the result. @revenueByCompany
,
ValueProperty to Revenue
and CategoryProperty to Company
. Set the Width to 100%
.
Revenue
.RevenueByMonth
custom method in the Page OnInitialized() method. Create a page property called revenueByMonth
from the result.@revenueByMonth
, and CategoryProperty to Month
.Revenue by employee
.RevenueByEmployee
custom method in the Page OnInitialized() method. Create a page property called revenueByEmployee
from the result.@revenueByEmployee
, and CategoryProperty to Employee
.Here is how the end result should look like.
As usual we start with the layout.
6
.RadzenCRMService
and change OnInitialized()
to be asynchronous. GetOpportunities
data source method. Do not select any columns.Home.razor.cs
, choose methods & design, select method OnInitializedAsync
, choose Query Builder from the properties panel and add Contact,OpportunityStatus
as expand
parameter.@context.Contact.FirstName
and @context.Contact.LastName
.
Set the Title to Contact
and SortProperty to Contact.FirstName
.Amount
. Set Template to @context.Amount.ToString("C")
.OpportunityStatus.Name
and Title to Status
.CloseDate
.Active Tasks
.GetTasks
data source method. Do not select any columns.We also need to include the User
property in the result of the GetTasks
operation.
Services\RadzenCRMService.Custom.cs
file.RadzenCRMService
class:partial void OnTasksRead(ref IQueryable<CRMBlazorServerRBS.Models.RadzenCRM.Task> items)
{
items = items.Include(item => item.Opportunity.User).Include(item => item.Opportunity.Contact);
}
Set also expand
parameter to Opportunity($expand=User,Contact)
.
Employee
and SortProperty to Opportunity.User.FirstName
. Edit its Template.@context.Opportunity.User.Picture
. Set Width and Height to 30px
and BorderRadius to 15px
.@context.Opportunity.User.FirstName
.@context.Opportunity.User.LastName
.Title
.Opportunity.Name
. Edit its Template.@context.Opportunity.Name
.
Go to Style and set Display to block
. This will make the next label appear on a new line.@context.Opportunity.Contact.FirstName
.@context.Opportunity.Contact.LastName
and end template editing.DueDate
.The Dashboard is now complete! Here is how it will look at runtime.
It is time for the finishing touches.
Radzen is free to use. You can also test the premium features for 15 days.
Download NowSelect theme: