Mobile Shop Project Part 19: Build an E-Commerce Admin Dashboard UI Setup in C# & Bootstrap 5

 


Understanding Your ASP.NET Core Project Structure

Looking at your Solution Explorer screenshot, your project perfectly leverages ASP.NET Core Areas to separate different parts of the application.

Where the Dashboard Code Lives

  • The Controller: You will be writing the backend code inside Areas/Admin/Controllers/DashboardController.cs. This isolates administrative statistics away from your main storefront HomeController.

  • The View: The dashboard layout markup will be placed inside a new folder structure: Areas/Admin/Views/Dashboard/Index.cshtml.

  • The View Models: The code file you just shared belongs inside your root ViewModels directory (or a subfolder inside it). Since View Models are lightweight data contracts (DTOs), both your root Controllers and your Admin Area Controllers can reference them easily.

Part 19: Building the Admin Dashboard

Step 1: Implementing the Analytical Dashboard View Models

Instead of passing heavy database domain models directly to the view—which risks leaking sensitive database schemas—you have created a clean, dedicated aggregation payload contract split across four functional tracking segments.

C# / ViewModels/DashboardViewModel.cs (Analytical Metrics)
using System;
using System.Collections.Generic;

namespace MobileShop.ViewModels
{
    public class DashboardViewModel
    {
        public int TotalOrders { get; set; }
        public int TotalProducts { get; set; }
        public int TotalCustomers { get; set; }
        public decimal TotalRevenue { get; set; }
        public int PendingOrders { get; set; }
        public int LowStockProducts { get; set; }
        public List<RecentOrderViewModel> RecentOrders { get; set; } = new List<RecentOrderViewModel>();
        public List<TopProductViewModel> TopProducts { get; set; } = new List<TopProductViewModel>();
        public List<MonthlySalesViewModel> MonthlySales { get; set; } = new List<MonthlySalesViewModel>();
    }

    public class RecentOrderViewModel
    {
        public int OrderId { get; set; }
        public string OrderNumber { get; set; } = string.Empty;
        public string CustomerName { get; set; } = string.Empty;
        public decimal TotalAmount { get; set; }
        public string Status { get; set; } = string.Empty;
        public DateTime OrderDate { get; set; }
    }

    public class TopProductViewModel
    {
        public int ProductId { get; set; }
        public string ProductName { get; set; } = string.Empty;
        public int UnitsSold { get; set; }
        public decimal Revenue { get; set; }
    }

    public class MonthlySalesViewModel
    {
        public string Month { get; set; } = string.Empty;
        public decimal Sales { get; set; }
        public int Orders { get; set; }
    }
}

Core Data Architecture Breakdown

  • High-Level KPI Metric Flags (Key Performance Indicators):

    public int TotalOrders { get; set; }
    public decimal TotalRevenue { get; set; }
    

    These scalar properties are designed to feed standard Bootstrap 4/5 numeric status cards. They provide an instant snapshot of the business health (such as counting orders or revenue amounts) as soon as the manager opens the portal.

  • Operational Risk Triggers (PendingOrders & LowStockProducts): These properties allow the admin view to create immediate action items. For instance, highlighting a red warning box if LowStockProducts is greater than zero forces the business owner to restock items quickly.

  • Nested Sub-Collection Matrices: Your main DashboardViewModel contains structured lists instead of flat lines:

    • RecentOrders: Feeds an instant transactional summary data table showing who bought what today.

    • TopProducts: Ranks device popularity by sales velocity, telling the business which mobile phone models are driving profit.

    • MonthlySales: Pairs months with numeric balances (Sales and Orders), which is exactly what charting plugins like Chart.js require to render interactive visual sales graphs.

Step 2: Implementing the Admin Dashboard Controller

  • Step 2 introduces the actual entry point of our backend administration site: the DashboardController. This controller bridges the gap between your secure data pipelines and the analytical dashboard view model you created in Step 1.

C# / Areas/Admin/Controllers/DashboardController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using MobileShop.Data;
using MobileShop.Models;
using MobileShop.Services;
using System.Threading.Tasks;

namespace MobileShop.Areas.Admin.Controllers
{
    [Area("Admin")]
    [Authorize(Roles = "Admin")]
    public class DashboardController : Controller
    {
        private readonly IReportService _reportService;
        private readonly ApplicationDbContext _context;
        private readonly UserManager<ApplicationUser> _userManager;

        public DashboardController(
            IReportService reportService,
            ApplicationDbContext context,
            UserManager<ApplicationUser> userManager)
        {
            _reportService = reportService;
            _context = context;
            _userManager = userManager;
        }

        public async Task<IActionResult> Index()
        {
            var dashboardData = await _reportService.GetDashboardDataAsync();
            return View(dashboardData);
        }
    }
}

Core Security & Architectural Breakdown

  • Area Routing Decoupling ([Area("Admin")]): This attribute explicitly tells ASP.NET Core's routing engine that this controller does not belong to the default root space. It maps requests to the /Admin/Dashboard pattern, successfully segregating administrative actions from consumer endpoints.

  • Role-Based Access Control / RBAC ([Authorize(Roles = "Admin")]): This is your primary defensive gatekeeper. Applying standard authentication via [Authorize] isn't enough for a backend panel. By appending Roles = "Admin", the framework intercepts incoming requests and cross-references the user's security claims against your Identity database. If a regular customer attempts to force-navigate to /Admin/Dashboard, they are blocked instantly with an HTTP 403 Forbidden response.

  • Multi-Service Dependency Injection Cluster: Your constructor pulls down three separate architectural dependencies:

    1. IReportService: Abstracted business intelligence layer dedicated to generating performance statistics.

    2. ApplicationDbContext: Direct database interface if ad-hoc entity validation is required.

    3. UserManager<ApplicationUser>: Provides programmatic hooks into your ASP.NET Core Identity store for managing employee or buyer account credentials.

  • Asynchronous Service Execution Flow (Index): The Index action method avoids blocking active threads. By calling await _reportService.GetDashboardDataAsync(), the web server is freed up to process other incoming storefront traffic while SQL Server computes the e-commerce sales tallies. Once calculated, the clean payload is passed directly to your administrative interface.

Step 3: Explaining the Administrative Dashboard UI

This UI combines high-level statistical status cards, interactive metrics charts, dynamic multi-conditional badges, and data iteration loops using Bootstrap 5 and Chart.js.

Razor / Areas/Admin/Views/Dashboard/Index.cshtml
@model DashboardViewModel
@{
    ViewData["Title"] = "Dashboard";
}

<div class="row">
    <div class="col-12">
        <h3 class="mb-4">Dashboard Overview</h3>
    </div>
</div>

<!-- Stats Cards -->
<div class="row g-4 mb-4">
    <div class="col-xl-3 col-md-6">
        <div class="card bg-primary text-white">
            <div class="card-body">
                <div class="d-flex justify-content-between align-items-center">
                    <div>
                        <h6 class="text-uppercase mb-1">Total Orders</h6>
                        <h3 class="mb-0">@Model.TotalOrders</h3>
                    </div>
                    <i class="bi bi-cart3 display-4 opacity-50"></i>
                </div>
            </div>
        </div>
    </div>
    <div class="col-xl-3 col-md-6">
        <div class="card bg-success text-white">
            <div class="card-body">
                <div class="d-flex justify-content-between align-items-center">
                    <div>
                        <h6 class="text-uppercase mb-1">Total Revenue</h6>
                        <h3 class="mb-0">RS @Model.TotalRevenue.ToString("N0")</h3>
                    </div>
                    <i class="bi bi-currency-rupee display-4 opacity-50"></i>
                </div>
            </div>
        </div>
    </div>
    <div class="col-xl-3 col-md-6">
        <div class="card bg-info text-white">
            <div class="card-body">
                <div class="d-flex justify-content-between align-items-center">
                    <div>
                        <h6 class="text-uppercase mb-1">Total Products</h6>
                        <h3 class="mb-0">@Model.TotalProducts</h3>
                    </div>
                    <i class="bi bi-phone display-4 opacity-50"></i>
                </div>
            </div>
        </div>
    </div>
    <div class="col-xl-3 col-md-6">
        <div class="card bg-warning text-white">
            <div class="card-body">
                <div class="d-flex justify-content-between align-items-center">
                    <div>
                        <h6 class="text-uppercase mb-1">Total Customers</h6>
                        <h3 class="mb-0">@Model.TotalCustomers</h3>
                    </div>
                    <i class="bi bi-people display-4 opacity-50"></i>
                </div>
            </div>
        </div>
    </div>
</div>

<div class="row g-4 mb-4">
    <div class="col-xl-3 col-md-6">
        <div class="card border-warning">
            <div class="card-body">
                <div class="d-flex justify-content-between align-items-center">
                    <div>
                        <h6 class="text-warning text-uppercase mb-1">Pending Orders</h6>
                        <h3 class="mb-0">@Model.PendingOrders</h3>
                    </div>
                    <i class="bi bi-clock-history display-4 text-warning opacity-50"></i>
                </div>
            </div>
        </div>
    </div>
    <div class="col-xl-3 col-md-6">
        <div class="card border-danger">
            <div class="card-body">
                <div class="d-flex justify-content-between align-items-center">
                    <div>
                        <h6 class="text-danger text-uppercase mb-1">Low Stock</h6>
                        <h3 class="mb-0">@Model.LowStockProducts</h3>
                    </div>
                    <i class="bi bi-exclamation-triangle display-4 text-danger opacity-50"></i>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- Charts Row -->
<div class="row g-4 mb-4">
    <div class="col-lg-8">
        <div class="card shadow-sm">
            <div class="card-header">
                <h5 class="mb-0"><i class="bi bi-graph-up"></i> Monthly Sales</h5>
            </div>
            <div class="card-body">
                <canvas id="salesChart" height="300"></canvas>
            </div>
        </div>
    </div>
    <div class="col-lg-4">
        <div class="card shadow-sm">
            <div class="card-header">
                <h5 class="mb-0"><i class="bi bi-trophy"></i> Top Products</h5>
            </div>
            <div class="card-body">
                <canvas id="topProductsChart" height="300"></canvas>
            </div>
        </div>
    </div>
</div>

<!-- Recent Orders & Top Products Table -->
<div class="row g-4">
    <div class="col-lg-8">
        <div class="card shadow-sm">
            <div class="card-header d-flex justify-content-between align-items-center">
                <h5 class="mb-0"><i class="bi bi-clock-history"></i> Recent Orders</h5>
                <a asp-controller="Orders" asp-action="Index" class="btn btn-sm btn-primary">View All</a>
            </div>
            <div class="card-body">
                <div class="table-responsive">
                    <table class="table table-hover">
                        <thead>
                            <tr>
                                <th>Order #</th>
                                <th>Customer</th>
                                <th>Amount</th>
                                <th>Status</th>
                                <th>Date</th>
                                <th>Actions</th>
                            </tr>
                        </thead>
                        <tbody>
                            @foreach (var order in Model.RecentOrders)
                            {
                                <tr>
                                    <td>@order.OrderNumber</td>
                                    <td>@order.CustomerName</td>
                                    <td>RS @order.TotalAmount.ToString("N0")</td>
                                    <td>
                                        <span class="badge @(order.Status switch {
                                            "Pending" => "bg-warning",
                                            "Processing" => "bg-info",
                                            "Shipped" => "bg-primary",
                                            "Delivered" => "bg-success",
                                            "Cancelled" => "bg-danger",
                                            _ => "bg-secondary"
                                        })">@order.Status</span>
                                    </td>
                                    <td>@order.OrderDate.ToString("MMM dd, HH:mm")</td>
                                    <td>
                                        <a asp-controller="Orders" asp-action="Details" asp-route-id="@order.OrderId" class="btn btn-sm btn-outline-primary">
                                            <i class="bi bi-eye"></i>
                                        </a>
                                    </td>
                                </tr>
                            }
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
    <div class="col-lg-4">
        <div class="card shadow-sm">
            <div class="card-header">
                <h5 class="mb-0"><i class="bi bi-trophy"></i> Top Selling Products</h5>
            </div>
            <div class="card-body">
                <div class="list-group list-group-flush">
                    @foreach (var product in Model.TopProducts)
                    {
                        <div class="list-group-item d-flex justify-content-between align-items-center">
                            <div>
                                <h6 class="mb-0">@product.ProductName</h6>
                                <small class="text-muted">@product.UnitsSold units sold</small>
                            </div>
                            <span class="badge bg-success">RS @product.Revenue.ToString("N0")</span>
                        </div>
                    }
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <script>
        // Monthly Sales Chart
        var salesCtx = document.getElementById('salesChart').getContext('2d');
        var salesChart = new Chart(salesCtx, {
            type: 'line',
            data: {
                labels: [@Html.Raw(string.Join(",", Model.MonthlySales.Select(m => $"'{m.Month}'")))],
                datasets: [{
                    label: 'Sales (RS )',
                    data: [@string.Join(",", Model.MonthlySales.Select(m => m.Sales))],
                    borderColor: 'rgb(75, 192, 192)',
                    backgroundColor: 'rgba(75, 192, 192, 0.2)',
                    tension: 0.4,
                    fill: true
                }, {
                    label: 'Orders',
                    data: [@string.Join(",", Model.MonthlySales.Select(m => m.Orders))],
                    borderColor: 'rgb(255, 99, 132)',
                    backgroundColor: 'rgba(255, 99, 132, 0.2)',
                    tension: 0.4,
                    yAxisID: 'y1'
                }]
            },
            options: {
                responsive: true,
                interaction: {
                    mode: 'index',
                    intersect: false
                },
                scales: {
                    y: {
                        type: 'linear',
                        display: true,
                        position: 'left',
                        title: {
                            display: true,
                            text: 'Sales (RS )'
                        }
                    },
                    y1: {
                        type: 'linear',
                        display: true,
                        position: 'right',
                        title: {
                            display: true,
                            text: 'Orders'
                        },
                        grid: {
                            drawOnChartArea: false
                        }
                    }
                }
            }
        });

        // Top Products Chart
        var productsCtx = document.getElementById('topProductsChart').getContext('2d');
        var productsChart = new Chart(productsCtx, {
            type: 'doughnut',
            data: {
                labels: [@Html.Raw(string.Join(",", Model.TopProducts.Select(p => $"'{p.ProductName}'")))],
                datasets: [{
                    data: [@string.Join(",", Model.TopProducts.Select(p => p.Revenue))],
                    backgroundColor: [
                        'rgba(255, 99, 132, 0.8)',
                        'rgba(54, 162, 235, 0.8)',
                        'rgba(255, 206, 86, 0.8)',
                        'rgba(75, 192, 192, 0.8)',
                        'rgba(153, 102, 255, 0.8)'
                    ]
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        position: 'bottom'
                    }
                }
            }
        });
    </script>
}

Core UI Components & Layout Architecture

  • The KPI Grid & High-Attention Risk Badges: You established two distinct rows of metric indicators. The top row tracks high-level corporate metrics (TotalOrders, TotalRevenue) using bright, full-bleed Bootstrap utility colors. Directly beneath it, you placed a row of custom border-alert warning cards (PendingOrders, LowStockProducts). This keeps operational issues front and center so store managers can immediately identify stock shortages.

  • Razor-to-JavaScript String Serialization Canvas Assemblies:

    labels: [@Html.Raw(string.Join(",", Model.MonthlySales.Select(m => $"'{m.Month}'")))]

    This is the most critical technical mechanism in the entire view. Chart.js runs purely on the client browser using vanilla JavaScript arrays. By using string.Join inside an @Html.Raw() envelope, your backend framework seamlessly flattens C# server collection structures into standardized, comma-delimited JavaScript strings at runtime before rendering the page.

  • Dual-Axis Chart Scale Optimization: Your monthly sales timeline features a sophisticated configurations matrix using a dual-axis scale (y and y1). This allows a single graph canvas to display total revenue currency metrics (in thousands) on the left vertical border, alongside standard single-digit transaction counts on the right axis—preventing order volumes from getting visually flattened by large revenue scale numbers.

  • C# Switch Statement Status Badges: Your recent orders table utilizes a modern C# switch expression block right inside the class parameter of your HTML span. It maps transactional database values to highly scannable, color-coded tracking badges (Processing to info-blue, Delivered to success-green), keeping administrative audits incredibly efficient.

Understanding Razor Hierarchies in Admin Areas

By introducing these infrastructure files inside the Areas/Admin/Views/ folder, you configure a scoped structural inheritance chain that applies explicitly to administrative views without impacting your client storefront.

  • _ViewImports.cshtml (Global Directives): This file houses shared @using directives and your crucial @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers. Placing it here ensures that all your Admin layouts can automatically use the asp-area, asp-controller, and asp-action tag helpers without manually declaring them in every file.

  • _ViewStart.cshtml (Automatic Hierarchical Mapping): By putting Layout = "_AdminLayout"; inside this file, ASP.NET Core automatically wires every view inside your Admin Area to utilize your master dashboard template by default.

  • Views/Shared/_AdminLayout.cshtml: Your main visual wrapper that houses the administrative interface framework.

Step 4 — Master Admin Layout Architecture

This master view replaces standard public storefront elements with a dense, sidebar-driven administrative application deck utilizing Bootstrap 5 utility flags and dynamic route inspection.

Razor / Areas/Admin/Views/Shared/_AdminLayout.cshtml
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Admin Dashboard</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" />
    <link rel="stylesheet" href="~/css/admin.css" asp-append-version="true" />
    @await RenderSectionAsync("Styles", required: false)
</head>
<body>
    <div class="wrapper">
        <!-- Sidebar -->
        <nav id="sidebar">
            <div class="sidebar-header">
                <h4 class="text-white"><i class="bi bi-speedometer2"></i> Admin Panel</h4>
            </div>

            <ul class="list-unstyled components">
                <li class="@((ViewContext.RouteData.Values["controller"]?.ToString() == "Dashboard") ? "active" : "")">
                    <a asp-area="Admin" asp-controller="Dashboard" asp-action="Index">
                        <i class="bi bi-speedometer2"></i> Dashboard
                    </a>
                </li>
                <li class="@((ViewContext.RouteData.Values["controller"]?.ToString() == "Products") ? "active" : "")">
                    <a asp-area="Admin" asp-controller="Products" asp-action="Index">
                        <i class="bi bi-phone"></i> Products
                    </a>
                </li>
                <li class="@((ViewContext.RouteData.Values["controller"]?.ToString() == "Categories") ? "active" : "")">
                    <a asp-area="Admin" asp-controller="Categories" asp-action="Index">
                        <i class="bi bi-tags"></i> Categories
                    </a>
                </li>
                <li class="@((ViewContext.RouteData.Values["controller"]?.ToString() == "Brands") ? "active" : "")">
                    <a asp-area="Admin" asp-controller="Brands" asp-action="Index">
                        <i class="bi bi-building"></i> Brands
                    </a>
                </li>
                <li class="@((ViewContext.RouteData.Values["controller"]?.ToString() == "Orders") ? "active" : "")">
                    <a asp-area="Admin" asp-controller="Orders" asp-action="Index">
                        <i class="bi bi-cart3"></i> Orders
                    </a>
                </li>
                <li class="@((ViewContext.RouteData.Values["controller"]?.ToString() == "Users") ? "active" : "")">
                    <a asp-area="Admin" asp-controller="Users" asp-action="Index">
                        <i class="bi bi-people"></i> Users
                    </a>
                </li>
                <li>
                    <a href="#reportsSubmenu" data-bs-toggle="collapse" class="dropdown-toggle">
                        <i class="bi bi-graph-up"></i> Reports
                    </a>
                    <ul class="collapse list-unstyled" id="reportsSubmenu">
                        <li>
                            <a asp-area="Admin" asp-controller="Dashboard" asp-action="SalesReport">Sales Report</a>
                        </li>
                        <li>
                            <a asp-area="Admin" asp-controller="Dashboard" asp-action="InventoryReport">Inventory Report</a>
                        </li>
                        <li>
                            <a asp-area="Admin" asp-controller="Dashboard" asp-action="TopProducts">Top Products</a>
                        </li>
                    </ul>
                </li>
            </ul>

            <div class="sidebar-footer">
                <a asp-area="" asp-controller="Home" asp-action="Index" class="btn btn-outline-light btn-sm w-100 mb-2">
                    <i class="bi bi-shop"></i> View Store
                </a>
                <form asp-area="" asp-controller="Account" asp-action="Logout" method="post" class="m-0">
                    <button type="submit" class="btn btn-outline-danger btn-sm w-100">
                        <i class="bi bi-box-arrow-right"></i> Logout
                    </button>
                </form>
            </div>
        </nav>

        <!-- Page Content -->
        <div id="content">
            <nav class="navbar navbar-expand-lg navbar-light bg-light">
                <div class="container-fluid">
                    <button type="button" id="sidebarCollapse" class="btn btn-dark">
                        <i class="bi bi-list"></i>
                    </button>
                    <div class="ms-auto">
                        <span class="navbar-text">
                            <i class="bi bi-person-circle"></i> @User.Identity?.Name
                        </span>
                    </div>
                </div>
            </nav>

            <div class="container-fluid p-4">
                @if (TempData["Success"] != null)
                {
                    <div class="alert alert-success alert-dismissible fade show" role="alert">
                        <i class="bi bi-check-circle-fill"></i> @TempData["Success"]
                        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
                    </div>
                }
                @if (TempData["Error"] != null)
                {
                    <div class="alert alert-danger alert-dismissible fade show" role="alert">
                        <i class="bi bi-exclamation-triangle-fill"></i> @TempData["Error"]
                        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
                    </div>
                }

                @RenderBody()
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="~/js/admin.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

Core UI & Engine Mechanisms Explained

  • Dynamic Navigation Highlighting (Active Route Matching):

    class="@((ViewContext.RouteData.Values["controller"]?.ToString() == "Products") ? "active" : "")"

    This block prevents your sidebar navigation links from feeling static. It reads the server's current routing context from the active request. If a shop manager clicks on "Products," the server evaluates the rule, appends the .active CSS style class directly to that row element, and updates the highlight style automatically.

  • Context-Driven Admin Claims Rendering (@User.Identity?.Name): The top horizontal header includes a metadata container rendering the identity name. This safely extracts claims token headers out of the current logged-in identity context. This ensures that whatever admin worker logs into the platform sees their exact administrative username reflected on the top right of the dashboard.

  • Dynamic Script & Styling Injection Boundaries:

    @await RenderSectionAsync("Scripts", required: false)

    Master layouts must remain lean. By including structural script placeholders at the bottom of the layout, you can inject dependencies like Chart.js locally only when loading the main dashboard. This prevents regular data table pages (like category lists) from suffering from bloated script loads.

  • Global Administrative Message Pipelines (TempData Integration): The template includes a universal messaging area right above @RenderBody(). Placing success and error alerts inside the master template guarantees that every page across your inventory system can reliably display confirmations—such as "Product Updated Successfully"—by simply dropping a value into the controller's TempData collection.

Step 5: Explaining the Admin Custom CSS Engine

Your CSS strategy relies on Flexbox architecture, state-driven interface selectors, and advanced typography hierarchies.

CSS / wwwroot/css/admin.css
/* Admin Dashboard Styles */
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #f5f6fa;
}

/* Wrapper */
.wrapper {
    display: flex;
    width: 100%;
    min-height: 100vh;
    align-items: stretch;
}

/* Sidebar Styles */
#sidebar {
    background: #2c3e50 !important;
    color: #fff;
    min-width: 220px;
    max-width: 220px;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    transition: all 0.3s ease;
    position: relative;
}

#sidebar.active {
    margin-left: -220px;
}

#sidebar .sidebar-header {
    background: #1a252f;
    padding: 14px 16px;
    flex-shrink: 0;
}

#sidebar .sidebar-header h4 {
    font-size: 1.1rem;
    margin: 0;
}

/* Navigation */
#sidebar ul.components {
    padding: 6px 0;
    flex: 1;
    overflow-y: auto;
}

#sidebar ul.components::-webkit-scrollbar {
    width: 4px;
}

#sidebar ul.components::-webkit-scrollbar-thumb {
    background: rgba(255,255,255,0.15);
    border-radius: 4px;
}

#sidebar ul li {
    margin-bottom: 1px;
}

#sidebar ul li a {
    padding: 9px 16px;
    font-size: 0.92rem;
    color: #adb5bd;
    text-decoration: none;
    display: block;
    transition: all 0.2s ease;
    border-left: 3px solid transparent;
    white-space: nowrap;
}

#sidebar ul li a:hover {
    color: #fff;
    background: rgba(255,255,255,0.06);
    border-left-color: #3498db;
}

#sidebar ul li.active a {
    color: #fff;
    background: rgba(52, 152, 219, 0.15);
    border-left-color: #3498db;
}

/* Dropdown submenu */
#sidebar ul ul {
    background: #1a252f;
    padding: 2px 0;
}

#sidebar ul ul li a {
    padding: 7px 16px 7px 40px;
    font-size: 0.85rem;
    color: #8e9aaf;
}

#sidebar ul ul li a:hover {
    color: #fff;
    background: rgba(255,255,255,0.04);
}

/* Dropdown toggle arrow */
#sidebar .dropdown-toggle::after {
    float: right;
    margin-top: 6px;
    font-size: 0.7rem;
}

/* Sidebar footer buttons */
#sidebar .sidebar-footer {
    padding: 10px 12px;
    border-top: 1px solid rgba(255,255,255,0.08);
    flex-shrink: 0;
    background: #2c3e50;
}

#sidebar .sidebar-footer .btn {
    padding: 6px 10px;
    font-size: 0.82rem;
}

/* Page Content */
#content {
    width: 100%;
    min-height: 100vh;
    transition: all 0.3s ease;
}

/* Top Navbar */
#content .navbar {
    padding: 10px 20px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}

#sidebarCollapse {
    padding: 4px 10px;
    font-size: 0.9rem;
}

/* Card Styles */
.card {
    border: none;
    border-radius: 10px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}

.card-header {
    background: #fff;
    border-bottom: 1px solid #e9ecef;
    padding: 12px 18px;
    font-weight: 600;
}

/* Stats Cards */
.card.bg-primary,
.card.bg-success,
.card.bg-info,
.card.bg-warning {
    border-radius: 10px;
}

/* Table Styles */
.table {
    font-size: 0.88rem;
}

.table thead th {
    font-weight: 600;
    text-transform: uppercase;
    font-size: 0.75rem;
    letter-spacing: 0.5px;
    padding: 10px 14px;
    background: #f8f9fa;
}

.table tbody td {
    padding: 10px 14px;
    vertical-align: middle;
}

/* Button Styles */
.btn {
    border-radius: 5px;
    font-weight: 500;
}

.btn-sm {
    padding: 0.25rem 0.5rem;
    font-size: 0.875rem;
}

/* Badge Styles */
.badge {
    padding: 0.4em 0.65em;
    font-weight: 500;
    font-size: 0.78rem;
}

/* Form Styles */
.form-control,
.form-select {
    border-radius: 5px;
    border: 1px solid #dee2e6;
    font-size: 0.9rem;
}

.form-control:focus,
.form-select:focus {
    border-color: #3498db;
    box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
}

/* Chart Container */
canvas {
    max-height: 300px;
}

/* Pagination */
.pagination .page-link {
    border: none;
    color: #2c3e50;
    padding: 0.5rem 0.75rem;
    font-size: 0.88rem;
}

.pagination .page-item.active .page-link {
    background-color: #3498db;
    color: #fff;
    border-radius: 5px;
}

/* Responsive */
@media (max-width: 768px) {
    #sidebar {
        margin-left: -220px;
        position: fixed;
        z-index: 1050;
        height: 100vh;
    }

    #sidebar.active {
        margin-left: 0;
    }

    #content {
        width: 100%;
    }
}

/* Animations */
.fade-in {
    animation: fadeIn 0.3s ease-in;
}

@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(10px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

Core Stylesheet Architecture Breakdown

  • The Flexible Layout Container (.wrapper & #sidebar): By declaring display: flex with align-items: stretch on the outer framework wrapper, you ensure that the navigation drawer and the main management canvas always maintain identical vertical height parameters—even when viewing a short data metrics list.

  • State-Driven Navigation Pseudo-Borders:

    #sidebar ul li a { border-left: 3px solid transparent; }
    #sidebar ul li.active a { border-left-color: #3498db; }
    

    Instead of shifting layout layouts on hover, you initialize a hidden (transparent) left border on every sidebar row. When your C# controller injects the .active class into the DOM, the CSS transitions the color properties smoothly to #3498db (flat blue), resulting in a high-end application behavior signature.

  • Component Flattening Override Matrix: Your styles actively strip down the generic Bootstrap look by removing standard border rules (border: none) on metric panels, rounding card containers out to 10px, and introducing clean, low-intensity box-shadow coefficients (rgba(0,0,0,0.08)). This provides a modern, flat-design feel.

  • Break-Point Desktop Displacements (@media Query Engine): The responsive breakpoint at 768px alters how the view responds to smaller viewports. On desktop computers, your sidebar pushes the content window over; on mobile hardware, the sidebar switches to position: fixed with a high stacking order (z-index: 1050), neatly tucking out of sight until the menu toggle is tapped.

Step 6: Explaining the Admin JavaScript Suite

This script balances traditional DOM manipulation via jQuery, HTML5 browser APIs, and non-blocking AJAX background service connectors.

JavaScript / wwwroot/js/admin.js
// Admin Dashboard JavaScript

$(document).ready(function () {
    // Sidebar toggle
    $('#sidebarCollapse').on('click', function () {
        $('#sidebar').toggleClass('active');
    });

    // Initialize tooltips
    var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
    tooltipTriggerList.map(function (tooltipTriggerEl) {
        return new bootstrap.Tooltip(tooltipTriggerEl);
    });

    // Initialize popovers
    var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
    popoverTriggerList.map(function (popoverTriggerEl) {
        return new bootstrap.Popover(popoverTriggerEl);
    });

    // Data table sorting
    $('.table-sortable th').on('click', function () {
        var table = $(this).parents('table').eq(0);
        var rows = table.find('tr:gt(0)').toArray().sort(comparer($(this).index()));
        this.asc = !this.asc;
        if (!this.asc) {
            rows = rows.reverse();
        }
        for (var i = 0; i < rows.length; i++) {
            table.append(rows[i]);
        }
    });

    function comparer(index) {
        return function (a, b) {
            var valA = getCellValue(a, index);
            var valB = getCellValue(b, index);
            return $.isNumeric(valA) && $.isNumeric(valB) ? valA - valB : valA.toString().localeCompare(valB);
        };
    }

    function getCellValue(row, index) {
        return $(row).children('td').eq(index).text();
    }

    // Confirm delete
    $('.confirm-delete').on('click', function (e) {
        if (!confirm('Are you sure you want to delete this item?')) {
            e.preventDefault();
        }
    });

    // Bulk actions
    $('#selectAll').on('change', function () {
        $('.select-item').prop('checked', $(this).prop('checked'));
    });

    // Image preview
    window.previewImage = function (input, previewId) {
        if (input.files && input.files[0]) {
            var reader = new FileReader();
            reader.onload = function (e) {
                $('#' + previewId).attr('src', e.target.result).show();
            };
            reader.readAsDataURL(input.files[0]);
        }
    };

    // Dynamic form fields
    window.addSpecificationField = function () {
        var html = `
            <div class="row g-3 specification-row mb-3">
                <div class="col-md-4">
                    <input type="text" name="specNames[]" class="form-control" placeholder="Name" required />
                </div>
                <div class="col-md-4">
                    <input type="text" name="specValues[]" class="form-control" placeholder="Value" required />
                </div>
                <div class="col-md-3">
                    <input type="text" name="specGroups[]" class="form-control" placeholder="Group" />
                </div>
                <div class="col-md-1">
                    <button type="button" class="btn btn-danger btn-sm" onclick="$(this).closest('.specification-row').remove()">
                        <i class="bi bi-trash"></i>
                    </button>
                </div>
            </div>
        `;
        $('#specificationsContainer').append(html);
    };

    // Order status update
    window.updateOrderStatus = function (orderId, status) {
        $.post('/Admin/Orders/UpdateStatus', { id: orderId, status: status }, function (data) {
            if (data.success) {
                location.reload();
            }
        });
    };

    // Export table to CSV
    window.exportTableToCSV = function (tableId, filename) {
        var csv = [];
        var rows = document.querySelectorAll('#' + tableId + ' tr');
        
        for (var i = 0; i < rows.length; i++) {
            var row = [];
            var cols = rows[i].querySelectorAll('td, th');
            
            for (var j = 0; j < cols.length; j++) {
                row.push(cols[j].innerText);
            }
            
            csv.push(row.join(','));
        }
        
        downloadCSV(csv.join('\n'), filename);
    };

    function downloadCSV(csv, filename) {
        var csvFile = new Blob([csv], { type: 'text/csv' });
        var downloadLink = document.createElement('a');
        downloadLink.download = filename;
        downloadLink.href = window.URL.createObjectURL(csvFile);
        downloadLink.style.display = 'none';
        document.body.appendChild(downloadLink);
        downloadLink.click();
        document.body.removeChild(downloadLink);
    }

    // Auto-hide alerts
    setTimeout(function () {
        $('.alert-dismissible').alert('close');
    }, 5000);

    // Chart.js defaults
    if (typeof Chart !== 'undefined') {
        Chart.defaults.responsive = true;
        Chart.defaults.maintainAspectRatio = false;
        Chart.defaults.plugins.legend.position = 'bottom';
    }

    // Date range picker
    $('.date-range').on('change', function () {
        var startDate = $('#startDate').val();
        var endDate = $('#endDate').val();
        
        if (startDate && endDate) {
            window.location.href = window.location.pathname + '?startDate=' + startDate + '&endDate=' + endDate;
        }
    });

    // Stock alert
    window.checkStock = function (productId) {
        $.get('/Admin/Products/CheckStock/' + productId, function (data) {
            if (data.stock <= 10) {
                showAlert('Low stock alert: Only ' + data.stock + ' units remaining!', 'warning');
            }
        });
    };

    // Print function
    window.printInvoice = function () {
        window.print();
    };

    // Responsive sidebar
    function handleResize() {
        if ($(window).width() <= 768) {
            $('#sidebar').addClass('active');
        } else {
            $('#sidebar').removeClass('active');
        }
    }

    $(window).resize(handleResize);
    handleResize();
});

// Utility functions
window.showAlert = function (message, type) {
    var alertHtml = `
        <div class="alert alert-${type} alert-dismissible fade show" role="alert">
            ${message}
            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        </div>
    `;
    $('.container-fluid').prepend(alertHtml);
    
    setTimeout(function () {
        $('.alert').alert('close');
    }, 5000);
};

window.formatCurrency = function (amount) {
    return 'RS ' + parseFloat(amount).toLocaleString('en-IN', {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2
    });
};

window.formatDate = function (dateString) {
    var date = new Date(dateString);
    return date.toLocaleDateString('en-IN', {
        year: 'numeric',
        month: 'short',
        day: 'numeric'
    });
};

Core Engine Features Explained

  • Client-Side Data Table Sorting Engine:

    $('.table-sortable th').on('click', function () { ... })

    Instead of forcing a slow server-side page reload every time an admin wants to sort data, this script evaluates columns inside the browser. It reads values via a custom getCellValue utility and matches arrays using localeCompare for text descriptions or math subtractions (valA - valB) for numeric values. It then instantly appends rows back to the DOM tree in the new order.

  • HTML5 FileReader Binary Stream Processing: The previewImage module taps directly into native browser capability. When an administrator selects a new smartphone image file for an item profile, the FileReader interceptor converts that local file into an in-memory string representation via readAsDataURL. It passes that data directly to the image source attribute, letting users verify their upload instantly without hitting your backend storage prematurely.

  • Dynamic Array Dom Injection: The addSpecificationField mechanism lets you manage deep device comparisons (like tracking variable RAM or Battery sizes). It leverages JavaScript template literals to build out a dynamic layout block complete with unique array notation brackets (specNames[], specValues[]). When submitted, the ASP.NET Core Model Binder automatically parses these matching inputs into a clean C# List contract inside your controller.

  • In-Memory CSV Report Generation: The exportTableToCSV tool scrapes plain text out of target table cell trees (td, th) and bundles them into lines separated by commas. It then leverages a native browser component known as a Blob (Binary Large Object) to mimic a file download link in memory, triggering a clean CSV save completely on the client side.

  • Non-Blocking Background Service Connectors (AJAX): Functions like updateOrderStatus and checkStock keep the admin experience smooth. By calling $.post and $.get asynchronously against endpoints like /Admin/Orders/UpdateStatus, data saves and updates happen in the background without forcing the administrator to lose their active page placement.

Step 7: Explaining the Area Route Mapping in Program.cs

This step establishes an explicit pattern match rule inside the global application builder routing table pipeline.

C# / Program.cs (Endpoint Routing)
app.MapControllerRoute(
    name: "areas",
    pattern: "{area:exists}/{controller=Dashboard}/{action=Index}/{id?}");

The Routing Architecture & URL Pattern Breakdown

To understand how ASP.NET Core resolves this route token by token, let's look at the mechanical anatomy of your new route engine mapping:

  • name: "areas": This assigns a distinct identification key to this routing rule. It tells the framework's internal link generation engine that whenever a Tag Helper uses an asp-area="..." parameter assignment, it should look up this specific configuration layout to construct the appropriate destination hyperlink.

  • {area:exists} (The Area Route Constraint): This is an essential performance and routing boundary safety measure. The routing engine checks if an incoming URL path contains a structural controller directory match inside the project's root Areas/ folder. If a user requests a URL like /Admin/Dashboard, the engine checks if an area named "Admin" exists. If it doesn't find a matching folder decorated with an [Area("Admin")] attribute, it ignores this rule completely and moves down to the default fallback routes.

  • {controller=Dashboard}/{action=Index} (Conventional Workspace Defaults): This parameter provides an elegant user experience. If a shop administrator types /Admin into their browser address bar, the route engine notices that the controller and action tokens are omitted. It automatically falls back to these default parameters, launching the DashboardController and running the Index action method implicitly.

  • {id?} (Optional Payload Constraint): The question mark token indicates that an extra parameter is fully optional. This rule configuration handles generic landing boards seamlessly, while also letting specific entity rows pass structural primary keys—such as a specific database record tracking ID—directly down to detail or update endpoints (e.g., /Admin/Products/Edit/45).

Comments