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 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.
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.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.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; }
}
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();
}
We will create a method which will calculate the monthly stats.
server\project.csproj
with Vusual Studio (or the server
directory with Visual Studio Code).server\Controllers\ServerMethodsController.cs
.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.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 }));
}
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.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.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; }
}
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>();
}
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 the BackgroundColor to Info
(from the predefined colors). Set FontSize to 48px
.
Revenue
,
Size to H4
, TextAlign to Right
and Margin to 0px
.
LAST MONTH
, Color to Cool grey
and FontSize to 12px
.
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.
Invoke method
. Pick MonthlyStats
from the dropdown.monthlyStats
and Value to ${result}
.${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
. Set BackgroundColor to Danger
.Opportunities
.${monthlyStats?.Opportunities.ToString()}
and Color to Danger
.account_balance_wallet
. Set BackgroundColor to Primary
.Average Deal Size
.${monthlyStats?.AverageDealSize.ToString()}
and Color to Primary
.thumb up
. Set BackgroundColor to Secondary
.Win Rate
.${monthlyStats?.Ratio.ToString("P")}
and Color to Secondary
.The final result should look like this at runtime.
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);
}
server\project.csproj
with Vusual Studio (or the server
directory with Visual Studio Code).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(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
}));
}
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>>();
}
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 Load event. Create a page property called revenueByCompany
with the ${result}
.
Column
, Data to ${revenueByCompany}
,
ValueProperty to Revenue
and CategoryProperty to Company
. Set the Width to 100%
.
Revenue
.RevenueByMonth
custom method in the Page Load event. Create a page property called revenueByMonth
with the ${result}
.Area
, Data to ${revenueByMonth}
, and CategoryProperty to Month
.Revenue by employee
.RevenueByEmployee
custom method in the Page Load event. Create a page property called revenueByEmployee
with the ${result}
.Bar
, Data to ${revenueByEmployee}
, and CategoryProperty to Employee
.Here is how the end result should look like.
As usual we start with the layout.
6
.First we need to get the data.
getOpportunities
data source method in the page Load event.CloseDate
in descending order.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.
${getOpportunitiesResult}
. Check AllowSorting.${data.Contact.FirstName} ${data.Contact.LastName}
.
Set the Title to Contact
and SortProperty to Contact.FirstName
.Amount
. Set Template to ${data.Amount.ToString("C")}
.OpportunityStatus.Name
and Title to Status
.CloseDate
.getTasks
data source method in the page Load event.DueDate
in descending order.getTasksResult
with Value ${result}
for server-side Blazor and ${result.Value.AsODataEnumerable()}
for client-side WebAssembly Blazor.We also need to include the User
property in the result of the getTasks
operation.
server\Services\CrmService.Custom.cs
file.CrmService
class:partial void OnTasksRead(ref IQueryable<Task> items)
{
items = items.Include(item => item.Opportunity.User).Include(item => item.Opportunity.Contact);
}
Set alost $expand
parameter to Opportunity($expand=User,Contact)
.
Then duplicate the other column that has the Opportunities DataGrid.
Active Tasks
.${getTasksResult}
.Employee
and SortProperty to Opportunity.User.FirstName
. Edit its Template.${data.Opportunity.User.Picture}
. Set Width and Height to 30px
and BorderRadius to 15px
.${data.Opportunity.User.FirstName}
.${data.Opportunity.User.LastName}
.
Title
.Opportunity.Name
. Edit its Template.${data.Opportunity.Name}
.
Go to Style and set Display to block
. This will make the next label appear on a new line.${data.Opportunity.Contact.FirstName}
.${data.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: