Welcome to Part 32 of our development series! Today we are entering a major new phase of our MobileShop project: Business Intelligence & Reporting.
In Step 1, we design a decoupled reporting layer by implementing the IReportService interface and its concrete class, ReportService. This service handles the analytical data gathering for our business dashboard—calculating historical sales revenue trends, managing real-time warehouse inventory thresholds, and tracking top-selling products using advanced aggregation techniques.
Here is the step-by-step breakdown of how this server-side reporting service processes your business metrics.
Step 1: Reporting Architecture
The Business Intelligence Aggregation Engine
Step-by-Step Code Explanation
The Interface Definition (IReportService)
public interface IReportService
{
Task<SalesReportViewModel> GetSalesReportAsync(DateTime startDate, DateTime endDate);
Task<InventoryReportViewModel> GetInventoryReportAsync();
Task<List<TopProductViewModel>> GetTopSellingProductsAsync(int count = 10);
}
Decoupling Strategy: By establishing an interface first, we decouple our business reporting logic from our controllers. This keeps the application maintainable, allows for easy Dependency Injection (DI), and makes unit testing much simpler.
All methods use an asynchronous design (
Task<>) to ensure that heavy data calculations won't freeze background threads or bottleneck database operations.
Sales Report Analytics Engine
public async Task<SalesReportViewModel> GetSalesReportAsync(DateTime startDate, DateTime endDate)
{
var orders = await _context.Orders
.Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate)
.ToListAsync();
Date Range Filtering: Filters orders instantly at the database level so the system only pulls invoices that fall between the chosen
startDateandendDate.Revenue Accounting Guard:
var totalSales = orders.Where(o => o.PaymentStatus == PaymentStatus.Paid).Sum(o => o.TotalAmount);This is a critical business rule logic check. We filter by
PaymentStatus.Paidto prevent pending, failed, or fraudulent checkout attempts from skewing our actual accounting revenue metrics.Daily Sales Trend Generation:
var dailySales = orders.GroupBy(o => o.OrderDate.Date) .Select(g => new DailySalesViewModel { ... })Groups the matching order records by their base date value (
.Datedrops the timestamp details) so it can aggregate total sales numbers and order frequencies day-by-day. This provides a clean dataset for rendering front-end charts or line graphs later on.
Real-Time Inventory Control Metrics
public async Task<InventoryReportViewModel> GetInventoryReportAsync()
{
var totalProducts = await _context.Products.CountAsync();
var lowStockProducts = await _context.Products
.Where(p => p.StockQuantity <= 10 && p.StockQuantity > 0)
...
Stock Health Tracking: Instantly counts total items and captures any products with stock levels between 1 and 10 units. This lets store managers quickly identify items that are running low and need to be restocked.
Out of Stock Alerting: Runs an efficient
CountAsynclookup specifically for rows whereStockQuantity == 0, providing a critical metric to track completely depleted inventory.
Bestsellers & Revenue Leaderboard
public async Task<List<TopProductViewModel>> GetTopSellingProductsAsync(int count = 10)
{
return _context.OrderItems
.Include(oi => oi.Product)
.GroupBy(oi => new { oi.ProductId, oi.Product.Name })
Composite Key Grouping: Digs into the
OrderItemstable and groups records by bothProductIdandProduct.Name.Metric Aggregation: For each grouped product bucket, it tallies up the absolute quantity of units sold via
.Sum(oi => oi.Quantity)and tracks total generated sales revenue using.Sum(oi => oi.TotalPrice).Top Truncation Optimization: Sorts the final leaderboard list in descending order based on volume sold (
OrderByDescending) and applies a.Take(count)limit parameter. This optimizes system performance by only pulling the top 10 rows into memory, rather than loading thousands of low-volume products.
In Step 2 of Part 32, we take the reporting service we built in the previous step and expose it to our administrative panel by creating the SalesReport Action Method inside your DashboardController.
This method handles incoming user-selected date ranges, falls back to a smart default timeline if no dates are provided, calls our async report service, and passes the compiled analytics data directly to the front-end view.
Here is the step-by-step breakdown of how this controller action bridges your data layer to the user interface.
Step 2: Dashboard Controller Integration
The Request-to-View Controller Workflow
Step-by-Step Code Explanation
Flexible Parameter Binding (Nullable DateTimes)
public async Task<IActionResult> SalesReport(DateTime? startDate, DateTime? endDate)
DateTime?: We purposefully declare the parameters as nullable types (using the?modifier). This allows the action method to accept traffic from two different entry points:When an admin first loads the page without picking any dates yet (parameters arrive as
null).When an admin uses a calendar date-picker to filter for a specific window (parameters arrive with explicit dates).
Smart Default Range Allocation (Null-Coalescing)
var start = startDate ?? DateTime.Now.AddDays(-30);
var end = endDate ?? DateTime.Now;
??(Null-Coalescing Operator): This checks if the incoming variables are null.If a store manager just opened the page,
startDateis null, so the system automatically defaults the start anchor to exactly 30 days ago (DateTime.Now.AddDays(-30)).Similarly, if
endDateis null, it defaults to the exact current day and time (DateTime.Now). This is a standard business dashboard pattern ensuring the manager is immediately presented with a fresh, rolling 30-day snapshot of their store's performance.
Service-Layer Data Processing
var report = await _reportService.GetSalesReportAsync(start, end);
Here, the controller calls the asynchronous
GetSalesReportAsyncmethod we injected from ourIReportService.By keeping the database heavy-lifting inside the service layer, the controller's only responsibility is handling the request parameters (
startandend), keeping your code clean, highly maintainable, and lightweight.
Delivering the Payload to the UI
return View(report);
The generated
SalesReportViewModelobject (containing total sales, order counts, average ticket size, and a list of daily sales) is packaged up and passed directly into the corresponding Razor View (SalesReport.cshtml).
In Step 3 of Part 32, we are building the front-end user interface: the SalesReport.cshtml Razor View.
This file serves as our administrative dashboard layout. It handles high-level Key Performance Indicators (KPIs) through Bootstrap stat cards, contains a query filter toolbar to dynamically shift date bounds, draws a dynamic interactive trend chart via Chart.js, and displays an organized tabular data audit grid.
Here is the step-by-step structural breakdown of how this client-side template processes your compiled view model.
Step 3: Sales Dashboard UI Anatomy
The Dashboard Visual Hierarchy
Step-by-Step Code Explanation
The Model Bind & Dynamic Filter Header
@model SalesReportViewModel
...
<form method="get" class="d-flex gap-2">
<input type="date" name="startDate" class="form-control form-control-sm" value="@Model.StartDate.ToString("yyyy-MM-dd")" />
<input type="date" name="endDate" class="form-control form-control-sm" value="@Model.EndDate.ToString("yyyy-MM-dd")" />
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-search"></i></button>
</form>
method="get": Crucial for filtering pages. Instead of submitting a hidden payload, it appends the selected dates directly to the browser URL string (e.g.,?startDate=2026-05-01&endDate=2026-05-31). This makes it easy for administrators to bookmark exact report intervals.value="@Model.StartDate.ToString("yyyy-MM-dd")": Binds our backend's tracking window limits back into the HTML5 input boxes. This keeps the current active search filter range perfectly visible on-screen after the page reloads.
The Executive KPI Block (Stat Cards)
<h3>RS @Model.TotalSales.ToString("N0")</h3>
...
<h3>@Model.TotalOrders</h3>
...
<h3>RS @Model.AverageOrderValue.ToString("N0")</h3>
Uses responsive Bootstrap columns (
col-md-4) to build three separate distinct metric containers matching a clean administrative workspace aesthetic.ToString("N0"): Format utility string modifier that normalizes raw currency numbers by adding proper thousands commas separator markers (e.g., transforming2500000into a highly legible2,500,000), adding instant visual polish to sales data.
The HTML5 Canvas Chart Anchor
<div class="card shadow-sm mb-4">
<div class="card-header"><h5 class="mb-0">Daily Sales</h5></div>
<div class="card-body">
<canvas id="dailySalesChart" height="300"></canvas>
</div>
</div>
Allocates an specialized HTML5 canvas area identifier element (
id="dailySalesChart"). This empty container acts as a mounting grid layout target that our Chart.js script hook will intercept and draw over during page runtime.
Tabular Data Breakdown Loop
@foreach (var day in Model.DailySales)
{
<tr>
<td>@day.Date.ToString("MMM dd, yyyy")</td>
<td>@day.OrderCount</td>
<td>RS @day.Sales.ToString("N0")</td>
</tr>
}
Evaluates the nested
DailySalescollection model. For every individual day object captured inside our date boundaries, it constructs a clean row output displaying localized tracking timestamps (MMM dd, yyyyoutputs clean strings likeJun 13, 2026), active loop frequencies, and explicit total numbers.
Chart.js Injected Script Architecture
@section Scripts {
<script>
var ctx = document.getElementById('dailySalesChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: [@Html.Raw(string.Join(",", Model.DailySales.Select(d => $"'{d.Date.ToString("MMM dd")}'")))],
datasets: [{
label: 'Sales (RS )',
data: [@string.Join(",", Model.DailySales.Select(d => d.Sales))],
@section Scripts: Mandates that the script block is cleanly rendered at the very base of the site's layout page, ensuring the external Chart.js library handles dependencies efficiently before drawing graphics.@Html.Raw(string.Join(...)): This is a brilliant integration trick. It uses LINQ projection to extract all the distinct dates from the model list, formats them as strings (like'Jun 01','Jun 02'), and injects them directly into the JavaScript engine's x-axis labels configuration without HTML encoding safety distortions.data: [@string.Join(...)]: Injects raw numerical sales values matching those identical dates as a basic, flat array sequence to map out the vertical column bars.
In Step 4 of Part 32, we expand our reporting module by exposing the real-time inventory metrics to our administration dashboard via the InventoryReport Action Method inside your DashboardController.
Compared to the sales report we built in the previous steps, the inventory report does not require custom date parameters. Instead, it serves as a live, real-time snapshot of your current warehouse stock levels. It queries your tracking service, compiles critical stock alerts, and pipes them directly to a dedicated manager layout view.
Here is the step-by-step structural breakdown of how this clean controller method operates.
Step 4: Inventory Controller Action Integration
The Real-Time Stock Monitoring Pipeline
Step-by-Step Code Explanation
Asynchronous Action Architecture
async Task<IActionResult>: Operating with asynchronous patterns is vital here. Counting stock quantities across extensive product catalogs can trigger heavy table locks on your SQL database. Making this method async ensures the thread pool is unblocked while the database completes its calculations, keeping your application fast and scalable under heavy admin user load.
Service-Layer Isolation
The controller coordinates the request by executing the asynchronous
GetInventoryReportAsyncmethod we registered in ourIReportServicearchitecture back in Step 1.By leaving the data-aggregation logic to the service layer, your controller is kept completely clean. It doesn't need to know how low stock is calculated (like our rule where stock $\le 10$ models triggers an alert); it simply collects the finished dataset package.
Delivering the Real-Time Payload to the View
The populated
InventoryReportViewModelobject is securely bound and sent down into the corresponding Razor View (InventoryReport.cshtml).This view model includes high-level counts like:
TotalProducts: Total number of models listed in the system.
LowStockCount: Total number of items running dangerously low.
OutOfStockCount: Total number of items completely sold out.
LowStockProducts: A list collection containing the specific names and stock quantities of those low items for immediate restocking.
In Step 5 of Part 32, we implement the front-end user interface for our inventory tracking: the InventoryReport.cshtml Razor View.
This view functions as an automated warehouse health monitor. It consumes our live InventoryReportViewModel data graph, structures high-level stock counts using color-coded Bootstrap KPI summary cards, and uses logical evaluation blocks to render a clear, actionable data table listing products that require immediate replenishment.
Step 5: Inventory Report Dashboard UI Anatomy
The Visual Hierarchy Layout
Step-by-Step UI Layout Explanation
Strongly Typed Model Binding & Header Context
@model InventoryReportViewModel
@{
ViewData["Title"] = "Inventory Report";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<h3><i class="bi bi-box-seam"></i> Inventory Report</h3>
</div>
@model InventoryReportViewModel: Connects this markup view strictly to our curated inventory report data contract, giving the Razor compilation engine direct autocomplete properties for inventory lists.bi bi-box-seam: Injects a clean Bootstrap Icon representing a cardboard shipping package, establishing a professional theme for stock-tracking pages.
Color-Coded Executive KPI Cards
<div class="card bg-primary text-white">...<h3>@Model.TotalProducts</h3></div>
<div class="card bg-warning text-white">...<h3>@Model.LowStockCount</h3></div>
<div class="card bg-danger text-white">...<h3>@Model.OutOfStockCount</h3></div>
This section leverages Bootstrap 5 row systems (
row g-4 mb-4) to establish a clean structural overview grid.The Visual Language Check: We intentionally map background color context classes (
bg-*) to reflect data urgency levels:bg-primary(Blue): Represents general metrics, displaying the total catalog size.bg-warning(Yellow/Orange): Draws attention to the low-stock counter, indicating items that are running low.bg-danger(Red): Signals an immediate emergency alert, highlighting items that are completely sold out and causing lost sales.
Dynamic Empty-State Condition Handling
@if (Model.LowStockProducts.Count > 0) { ... }
else { <p class="text-muted mb-0">All products have sufficient stock.</p> }
Before attempting to parse a data table on the screen, the view evaluates if there are any records inside the
LowStockProductscollection loop.If the list is empty (
Count == 0), the table structure stays completely hidden. Instead, it renders a clean fallback paragraph informing managers that everything on the shelves is completely healthy, saving screen space and improving user experience.
The Core Alert Data Table & Conditional Status Badges
@foreach (var product in Model.LowStockProducts)
{
<tr>
<td>@product.ProductId</td>
<td>@product.ProductName</td>
<td><span class="badge bg-warning">@product.CurrentStock</span></td>
<td>
@if (product.CurrentStock == 0)
{
<span class="badge bg-danger">Out of Stock</span>
}
else { <span class="badge bg-warning">Low Stock</span> }
</td>
</tr>
}
Inside our data loop iteration, we display individual rows for each low-stock tracking item.
Nested Razor Logic Layer: Inside the final
<td>column, the view evaluates the live@product.CurrentStockvalue inline. If the stock has fully dropped down to0, it renders a bright redOut of Stockbadge; if it is anywhere between1and10, it labels it asLow Stockin yellow. This makes it incredibly easy for administrators to skim the sheet and instantly prioritize ordering items that are entirely sold out.
In Step 6 of Part 32, we implement the final core component of our business intelligence module: the TopProducts Action Method inside the administrative DashboardController.
This method handles requests for your platform's sales leaderboard. It instructs our analytical service layer to extract your top-performing products based on sales velocity and total gross revenue, handles tracking constraints, and forwards the collection to a storefront analytics dashboard view.
Here is the step-by-step structural breakdown of how this controller method operates.
Step 6: Bestsellers Leaderboard Controller Integration
The Leaderboard Aggregation Pipeline
Step-by-Step Code Explanation
Asynchronous Task Signature
public async Task<IActionResult> TopProducts()
Performance Architecture: Compiling data leaderboards across relational order histories requires scanning and summing up entire transactional index logs. Declaring this action method as an asynchronous
Taskkeeps the application responsive and lightweight, ensuring your web server never bottlenecks thread resources while computing heavy statistics.
Constrained Range Ingestion Processing
var products = await _reportService.GetTopSellingProductsAsync(20);
The controller invokes the
GetTopSellingProductsAsyncmethod we registered in ourIReportServicearchitecture back in Step 1.The Scale Parameter (
20): Notice that we pass an explicit parameter integer value of20into the service method call. This overrides the default service fallback threshold of 10 items. This allows your primary dashboard home screen to show a tight "Top 10" snapshot, while this dedicated deep-dive reporting page presents an expanded list of your top 20 flagship items for deeper business analysis.
Delivering the Ranked List to the View
return View(products);
The compiled collection (
List<TopProductViewModel>) is securely bound and sent down into the corresponding Razor View (TopProducts.cshtml).Each individual data node passed inside this collection payload contains everything needed to render a rich leaderboard:
ProductId: The unique primary key index tracking the specific inventory item.ProductName: The public title text string identifying the mobile device.UnitsSold: The aggregated count of absolute hardware items processed through your checkouts.Revenue: The gross calculated currency volume generated by those specific sales.
In Step 7 of Part 32, we implement the front-end user interface for our sales leaderboard: the TopProducts.cshtml Razor View.
This view functions as a high-end product analytics console. It handles a multi-axis data display, showing a main, visual comparison chart alongside a compact "Top 3 Podium" ranking sidebar list group. It also features a fallback detailed grid layout displaying units sold and total generated store revenue.
Step 7: Bestsellers Dashboard UI Anatomy
The Leaderboard Panel Layout Hierarchy
Step-by-Step UI Layout Explanation
Multi-Column Dashboard Split
<div class="col-lg-8"> </div>
<div class="col-lg-4"> </div>
</div>
Uses an asymmetric Bootstrap 5 grid layout scheme (
col-lg-8andcol-lg-4). This design prioritizes horizontal visual data charts in the wide section, while keeping quick list summaries accessible on the right side.
Conditional "Top 3 Podium" Styling Loop
{
<span class="badge bg-@(i < 3 ? "warning" : "secondary") me-2">#@(i + 1)</span>
}
Index Pointer Tracking (
i + 1): Because standard C# loops are zero-indexed ($i = 0$), we display human-readable leaderboard values by adding an offset calculation (#@(i + 1)), which prints#1,#2,#3.Ternary Context Color Class: We apply an intentional conditional check (
i < 3 ? "warning" : "secondary"). This highlights your top 3 flagship items with a distinct gold badge (bg-warning), while assigning a neutral gray look (bg-secondary) to items ranked 4th and below.
Advanced Multi-Axis Chart.js Integration Script
datasets: [{
label: 'Units Sold',
data: [@string.Join(",", Model.Select(p => p.UnitsSold))],
backgroundColor: 'rgba(46, 204, 113, 0.7)' // Green
}, {
label: 'Revenue (RS thousands)',
data: [@string.Join(",", Model.Select(p => (p.Revenue / 1000).ToString("F0")))],
backgroundColor: 'rgba(155, 89, 182, 0.5)', // Purple
yAxisID: 'y1'
}]
Data Scale Mapping (
p.Revenue / 1000): This addresses a common charting problem: charting a small number (like 50 units sold) alongside a massive number (like 1,500,000 RS in revenue) flattens the smaller bar completely. To solve this, we divide the revenue numbers by1000to scale down the bars, labeling the dataset explicitly as Revenue (RS thousands).Dual Independent Y-Axes (
yAxisID: 'y1'): To make the chart easy to read, we define a second, separate axis config rule down in the options parameters:scales: {y: { beginAtZero: true, position: 'left' }, y1: { beginAtZero: true, position: 'right', grid: { drawOnChartArea: false } } }This creates a split chart: the left-hand Y-axis measures volume numbers for the green Units Sold columns, while the right-hand Y-axis maps monetary thresholds for the purple Revenue bars. This approach prevents data distortion and makes tracking performance simple.
In Step 8 of Part 32, we tie our entire business intelligence reporting module together by integrating it into the master _AdminLayout.cshtml navigation sidebar.
Instead of adding three separate, bulky links that take up valuable sidebar space, this code implements a clean, collapsible Dropdown Submenu using Bootstrap 5. It keeps the administrative cockpit organized by grouping all your analytical reports under a single "Reports" parent tab.
Here is the step-by-step structural breakdown of how this navigation menu operates.
Step 8: Collapsible Sidebar Menu Architecture
The Dropdown UI Hierarchy
Step-by-Step Code Explanation
The Parent Toggle Trigger
<i class="bi bi-graph-up"></i> Reports
</a>
data-bs-toggle="collapse": This native Bootstrap 5 data attribute tells the client-side JavaScript engine to treat this anchor tag as a toggle switch for a collapsible container instead of a traditional page hyperlink.
href="#reportsSubmenu": Specifies the exact target ID that this button is responsible for opening and closing. The
#symbol indicates a CSS ID selector match.class="dropdown-toggle": Automatically adds a small arrow chevron indicator ($\lor$) next to the word "Reports", signaling to the admin that this item expands into a sub-list.
The Hidden Submenu Container
class="collapse": This Bootstrap layout utility ensures that the sub-menu container stays completely hidden out of sight by default when the administrative dashboard first loads.
id="reportsSubmenu": This bridges the toggle to the list. This identity token matches the parent's
hrefparameter exactly. When the parent link is clicked, Bootstrap smoothly animates this specific<ul>block open or closed.list-unstyled: Strips away standard HTML default bullet points, allowing you to style the nested options cleanly.
Strongly Typed Area & Route Tag Helpers
asp-area="Admin": This is highly important for large enterprise setups. It explicitly tells the routing engine to look inside your project's Admin Area architecture bundle rather than the default root layout folder.
asp-controller="Dashboard" & asp-action="...": Maps out the exact destination logic paths. The framework automatically parses these helper rules to generate clean, SEO-friendly target URLs at runtime:
Sales Report maps to
/Admin/Dashboard/SalesReportInventory Report maps to
/Admin/Dashboard/InventoryReportTop Products maps to
/Admin/Dashboard/TopProducts

Comments
Post a Comment