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.
Start FreeSelect theme: