Part 20: Build an Admin Order Tracking & Management Panel in ASP.NET Core MVC


 Welcome to Part 20 of our Mobile Shop development series! Today, we are shifting our focus toward high-performance back-office order processing. In this first step, we implement the OrdersController, which serves as the core command center for viewing, filtering, and paging through thousands of incoming client transactions.

Part 20: Step 1 — Order Tracking Architecture

This controller handles high-volume order records by utilizing standard LINQ query postponement (AsQueryable) alongside multi-conditional server filtering and index pagination.

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

namespace MobileShop.Areas.Admin.Controllers
{
    [Area("Admin")]
    [Authorize(Roles = "Admin")]
    public class OrdersController : Controller
    {
        private readonly ApplicationDbContext _context;

        public OrdersController(ApplicationDbContext context)
        {
            _context = context;
        }

        public async Task<IActionResult> Index(string? status, string? search, int page = 1)
        {
            var query = _context.Orders
                .Include(o => o.User)
                .Include(o => o.OrderItems)
                .AsQueryable();

            if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<OrderStatus>(status, out var orderStatus))
                query = query.Where(o => o.Status == orderStatus);

            if (!string.IsNullOrWhiteSpace(search))
                query = query.Where(o => o.OrderNumber.Contains(search) || 
                                        (o.User != null && (o.User.Email.Contains(search) || o.User.FullName.Contains(search))));

            var pageSize = 20;
            var totalItems = await query.CountAsync();
            var orders = await query
                .OrderByDescending(o => o.OrderDate)
                .Skip((page - 1) * pageSize)
                .Take(pageSize)
                .ToListAsync();

            ViewBag.Statuses = Enum.GetNames(typeof(OrderStatus));
            ViewBag.CurrentStatus = status;
            ViewBag.Search = search;
            ViewBag.CurrentPage = page;
            ViewBag.TotalPages = (int)Math.Ceiling(totalItems / (double)pageSize);

            return View(orders);
        } 
    }
}

Core Backend Patterns Explained

  • Area Isolation & Dual-Layer Security Guards:

    [Area("Admin")]
    [Authorize(Roles = "Admin")]
    

    The code uses standard ASP.NET Core security decoration. The [Area("Admin")] attribute routes incoming requests through the area patterns we established in our pipeline earlier. Meanwhile, [Authorize(Roles = "Admin")] validates incoming session claims tokens. If an unauthenticated user or standard client tries to brute-force access /Admin/Orders, the framework immediately drops the connection and returns a access rejection response.

  • Deferred Query Execution Pipeline (AsQueryable):

    The method builds an initial execution tree by declaring .AsQueryable(). Instead of hitting the database immediately with high-overhead tables, it constructs an abstract expression layout structure. It appends dynamic constraints conditionally (like status filters and keyword search inputs) entirely in memory, without executing any database operations.

  • Advanced Multi-Column Keyword Searching:

    query = query.Where(o => o.OrderNumber.Contains(search) || ... )

    The lookup method offers an excellent user experience. It allows shop manager workers to type an order hash sequence, an account email identifier, or a profile full name into a single unified search field. The system translates this expression block into optimized relational database SQL LIKE conditions.

  • Server-Side Pagination Math Engine:

    To maintain system performance on high-volume stores, this code avoids transferring whole database records over the network. It calculates exact segment skips using the following formula:

    By running .Skip().Take(), the database server processes the records internally and only streams the matching 20 row profiles back to your web application instance.

  • State Persistence UI Handshakes (ViewBag Matrix):

    The script populates five individual view parameters before passing data to the view template. Storing dynamic properties like ViewBag.CurrentPage and ViewBag.TotalPages provides the frontend rendering engine with everything it needs to maintain active navigation states and preserve search terms across page loads.

Step 2 — Building the Orders Data Grid Interface

This view template handles user inputs, preserves search filtering configurations, renders dynamic contextual data layouts, and maps out conditional multi-parameter page controls.

Razor / Areas/Admin/Views/Orders/Index.cshtml
@model List<Order>
@{
    ViewData["Title"] = "Orders";
}

<div class="d-flex justify-content-between align-items-center mb-4">
    <h3><i class="bi bi-cart3"></i> Orders</h3>
</div>

<!-- Filters -->
<div class="card mb-4">
    <div class="card-body">
        <form method="get" class="row g-3">
            <div class="col-md-4">
                <input type="text" name="search" class="form-control" placeholder="Search order # or customer..." value="@ViewBag.Search" />
            </div>
            <div class="col-md-3">
                <select name="status" class="form-select">
                    <option value="">All Statuses</option>
                    @foreach (var status in ViewBag.Statuses)
                    {
                        <option value="@status" selected="@(ViewBag.CurrentStatus == status ? "selected" : null)">@status</option>
                    }
                </select>
            </div>
            <div class="col-md-2">
                <button type="submit" class="btn btn-outline-primary w-100"><i class="bi bi-search"></i> Filter</button>
            </div>
        </form>
    </div>
</div>

<div class="card shadow-sm">
    <div class="table-responsive">
        <table class="table table-hover mb-0">
            <thead class="table-dark">
                <tr>
                    <th>Order #</th>
                    <th>Customer</th>
                    <th>Items</th>
                    <th>Total</th>
                    <th>Status</th>
                    <th>Payment</th>
                    <th>Date</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var order in Model)
                {
                    <tr>
                        <td><strong>@order.OrderNumber</strong></td>
                        <td>
                            @if (order.User != null)
                            {
                                <span>@order.User.FullName</span>
                                <br /><small class="text-muted">@order.User.Email</small>
                            }
                            else
                            {
                                <span class="text-muted">Guest</span>
                            }
                        </td>
                        <td>@order.OrderItems.Count items</td>
                        <td>RS @order.TotalAmount.ToString("N0")</td>
                        <td>
                            <span class="badge @(order.Status switch {
                                OrderStatus.Pending => "bg-warning",
                                OrderStatus.Processing => "bg-info",
                                OrderStatus.Shipped => "bg-primary",
                                OrderStatus.Delivered => "bg-success",
                                OrderStatus.Cancelled => "bg-danger",
                                _ => "bg-secondary"
                            })">@order.Status</span>
                        </td>
                        <td>
                            <span class="badge @(order.PaymentStatus == PaymentStatus.Paid ? "bg-success" : "bg-warning")">
                                @order.PaymentStatus
                            </span>
                        </td>
                        <td>@order.OrderDate.ToString("MMM dd, yyyy HH:mm")</td>
                        <td>
                            <a asp-action="Details" asp-route-id="@order.Id" class="btn btn-sm btn-outline-primary">
                                <i class="bi bi-eye"></i>
                            </a>
                            <a asp-action="Invoice" asp-route-id="@order.Id" class="btn btn-sm btn-outline-info" target="_blank">
                                <i class="bi bi-printer"></i>
                            </a>
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    </div>
</div>

@if (ViewBag.TotalPages > 1)
{
    <nav class="mt-3">
        <ul class="pagination justify-content-center">
            @for (int i = 1; i <= ViewBag.TotalPages; i++)
            {
                <li class="page-item @(i == ViewBag.CurrentPage ? "active" : "")">
                    <a class="page-link" asp-action="Index" asp-route-page="@i" asp-route-status="@ViewBag.CurrentStatus" asp-route-search="@ViewBag.Search">@i</a>
                </li>
            }
        </ul>
    </nav>
}

Core UI Template Framework Patterns Explained

  • State Persistence Input Form Preservation:

    <input type="text" name="search" ... value="@ViewBag.Search" />

    When an administrator applies a filter, the browser submits a GET request. To prevent the text search input and dropdown selection options from resetting to blank states after the page reloads, the markup binds the current search value using @ViewBag.Search and applies an inline tertiary assignment evaluation matrix inside the @foreach loop: selected="@(ViewBag.CurrentStatus == status ? "selected" : null)".

  • Inline C# 8.0 Pattern Matching Layout Badges:

    @(order.Status switch { OrderStatus.Pending => "bg-warning", ... })

    Instead of utilizing bloated and repetitive multi-line nested conditional @if/else logic structures inside your table markup rows, the code introduces an optimized, clean inline C# Switch Expression. It evaluates the order's backend state enum values instantly and resolves them into designated contextual Bootstrap color badge flags (bg-warning, bg-success).

  • Graceful Relational View Property Validation: The table checks if order.User != null inside each row before trying to pull profile fields. This defensive programming approach guarantees that if an entry exists as an anonymous guest checkout or if an account record is historical or missing, the page renders a clean layout placeholder (Guest) rather than crashing the thread with a severe NullReferenceException.

  • Contextual Stateful Pagination Engine Builder:

    <a ... asp-route-page="@i" asp-route-status="@ViewBag.CurrentStatus" asp-route-search="@ViewBag.Search">@i</a>

    A common design flaw is losing active search filters when clicking on a page layout number. This template addresses that by mapping out compound routing keys inside the loop anchors using tag helpers. When an operator clicks on page 3, the resulting URL tracks all parameters together: /Admin/Orders?page=3&status=Shipped&search=Iphone.

Step 3 — The Invoice Data-Aggregation Engine

This backend action method leverages eager loading mechanics to assemble all the relevant purchase information—including user details, line items, and product catalog metadata—in a single database round-trip.

C# / Areas/Admin/Controllers/OrdersController.cs (Invoice Action)
public async Task<IActionResult> Invoice(int id)
{
    var order = await _context.Orders
        .Include(o => o.User)
        .Include(o => o.OrderItems)
            .ThenInclude(oi => oi.Product)
        .FirstOrDefaultAsync(o => o.Id == id);

    if (order == null)
        return NotFound();

    return View(order);
}

Core Data Retrieval Mechanics Explained

  • Multi-Level Relational Eager Loading (ThenInclude):

    .Include(o => o.User)
    .Include(o => o.OrderItems)
    .ThenInclude(oi => oi.Product)
    

    A standard database query against the Orders table only pulls basic columns like order dates and total amounts. To populate a professional commercial receipt, you must traverse multiple foreign keys.

    • .Include(o => o.User) performs an SQL join to attach buyer contact details.

    • .Include(o => o.OrderItems) grabs the invoice line items.

    • The .ThenInclude(oi => oi.Product) chain is the magic link: it steps inside the collection of line items to pull the exact descriptive name, pricing model, and branding images of each mobile phone item purchased.

  • Preventing the N+1 Database Query Performance Trap:

    Without utilizing explicit .Include paths, referencing an item name loop like item.Product.Name inside your HTML razor view would trigger an isolated database query query for every single line item on the receipt. By loading these relationships eagerly upfront, your application reads all data in one optimized database round-trip.

  • Defensive Boundary Validations (NotFound Check):

    If an operator modifies a URL manually or passes an invalid document tracking sequence number (e.g., /Admin/Orders/Invoice/999999), the server safely intercepts the missing record state via if (order == null) and returns a standard HTTP 404 status code instead of triggering an unhandled object exception.

Step 4 — Engineering the Print-Optimized Invoice Template

This view functions independently from your administrative theme architecture by breaking out of standard layout rendering engines and utilizing raw styling sheets modified specifically for paper printing media outputs.

Razor / Areas/Admin/Views/Orders/Invoice.cshtml
@model Order
@{
    ViewData["Title"] = "Invoice";
    Layout = null;
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Invoice - @Model.OrderNumber</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <style>
        body {
            background: #f5f5f5;
            padding: 20px;
        }

        .invoice-container {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            padding: 40px;
            box-shadow: 0 0 20px rgba(0,0,0,0.1);
        }

        .invoice-header {
            border-bottom: 2px solid #007bff;
            padding-bottom: 20px;
            margin-bottom: 30px;
        }

        .company-info h2 {
            color: #007bff;
            margin-bottom: 5px;
        }

        .invoice-title {
            text-align: right;
        }

        .invoice-title h1 {
            color: #333;
            font-size: 2.5rem;
            margin-bottom: 0;
        }

        .table th {
            background-color: #f8f9fa;
        }

        .total-row {
            background-color: #007bff;
            color: white;
            font-size: 1.1rem;
        }

        @@media print {
            body {
                background: white;
                padding: 0;
            }

            .invoice-container {
                box-shadow: none;
                padding: 20px;
            }

            .no-print {
                display: none !important;
            }

            .table th {
                background-color: #f8f9fa !important;
                -webkit-print-color-adjust: exact;
            }

            .total-row {
                background-color: #007bff !important;
                color: white !important;
                -webkit-print-color-adjust: exact;
            }
        }
    </style>
</head>
<body>
    <div class="invoice-container">
        <div class="no-print mb-3 d-flex justify-content-end gap-2">
            <button onclick="window.print()" class="btn btn-primary">
                <i class="fas fa-print"></i> Print Invoice
            </button>
            <a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-secondary">
                <i class="fas fa-arrow-left"></i> Back to Order
            </a>
        </div>

        <div class="invoice-header">
            <div class="row">
                <div class="col-6 company-info">
                    <h2>MobileShop</h2>
                    <p class="mb-0">123 Phone Street, Tech City</p>
                    <p class="mb-0">Phone: +91 1234567890</p>
                    <p class="mb-0">Email: support@mobileshop.com</p>
                </div>
                <div class="col-6 invoice-title text-end">
                    <h1>INVOICE</h1>
                    <p class="mb-0"><strong>Order #:</strong viewers> @Model.OrderNumber</p>
                    <p class="mb-0"><strong>Date:</strong> @Model.OrderDate.ToString("MMM dd, yyyy")</p>
                    <p class="mb-0"><strong>Status:</strong> @Model.Status</p>
                </div>
            </div>
        </div>

        <div class="row mb-4">
            <div class="col-6">
                <h6 class="text-muted text-uppercase fw-bold mb-3">Bill To:</h6>
                @if (Model.User != null)
                {
                    <p class="mb-1 fw-bold">@Model.User.FullName</p>
                    <p class="mb-1">@Model.User.Email</p>
                    <p class="mb-1">@Model.User.PhoneNumber</p>
                }
                else
                {
                    <p class="mb-1 fw-bold">Guest Customer</p>
                }
            </div>
            <div class="col-6">
                <h6 class="text-muted text-uppercase fw-bold mb-3">Ship To:</h6>
                <p class="mb-1 fw-bold">@Model.ShippingAddress</p>
                <p class="mb-1">@Model.ShippingCity, @Model.ShippingPostalCode</p>
                <p class="mb-1">@Model.ShippingCountry</p>
                @if (!string.IsNullOrEmpty(Model.ShippingPhone))
                {
                    <p class="mb-1">Phone: @Model.ShippingPhone</p>
                }
            </div>
        </div>

        <table class="table table-bordered">
            <thead>
                <tr>
                    <th style="width: 50%;">Item Description</th>
                    <th class="text-center">Quantity</th>
                    <th class="text-end">Unit Price</th>
                    <th class="text-end">Amount</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var item in Model.OrderItems)
                {
                    <tr>
                        <td>
                            @if (item.Product != null)
                            {
                                <div class="fw-bold">@item.Product.Name</div>
                                <small class="text-muted">@item.Product.Model</small>
                            }
                            else
                            {
                                <span>Product #@item.ProductId</span>
                            }
                        </td>
                        <td class="text-center">@item.Quantity</td>
                        <td class="text-end">RS @item.UnitPrice.ToString("N2")</td>
                        <td class="text-end">RS @item.TotalPrice.ToString("N2")</td>
                    </tr>
                }
            </tbody>
            <tfoot>
                <tr>
                    <td colspan="3" class="text-end"><strong>Subtotal:</strong></td>
                    <td class="text-end">RS @Model.Subtotal.ToString("N2")</td>
                </tr>
                <tr>
                    <td colspan="3" class="text-end"><strong>Tax Amount:</strong></td>
                    <td class="text-end">RS @Model.TaxAmount.ToString("N2")</td>
                </tr>
                <tr>
                    <td colspan="3" class="text-end"><strong>Shipping Cost:</strong></td>
                    <td class="text-end">RS @Model.ShippingCost.ToString("N2")</td>
                </tr>
                @if (Model.DiscountAmount > 0)
                {
                    <tr>
                        <td colspan="3" class="text-end text-success"><strong>Discount:</strong></td>
                        <td class="text-end text-success">-RS @Model.DiscountAmount.ToString("N2")</td>
                    </tr>
                }
                <tr class="total-row">
                    <td colspan="3" class="text-end"><strong>Total Amount:</strong></td>
                    <td class="text-end"><strong>RS @Model.TotalAmount.ToString("N2")</strong></td>
                </tr>
            </tfoot>
        </table>

        <div class="row mt-4">
            <div class="col-12">
                <h6 class="text-muted text-uppercase fw-bold mb-2">Payment Information</h6>
                <p class="mb-1"><strong>Method:</strong> @Model.PaymentMethod</p>
                <p class="mb-1"><strong>Status:</strong> @Model.PaymentStatus</p>
                @if (!string.IsNullOrEmpty(Model.TransactionId))
                {
                    <p class="mb-1"><strong>Transaction ID:</strong> @Model.TransactionId</p>
                }
            </div>
        </div>

        <div class="mt-5 pt-4 border-top text-center text-muted">
            <p class="mb-0">Thank you for shopping with MobileShop!</p>
            <p class="small">For any queries, please contact our customer support.</p>
        </div>
    </div>

    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script>
        window.onload = function() {
            // Auto-print if query parameter present
            const urlParams = new URLSearchParams(window.location.search);
            if (urlParams.get('print') === 'true') {
                setTimeout(() => window.print(), 500);
            }
        };
    </script>
</body>
</html>

Core Frontend Sheet Features Explained

  • Disabling Shared Layout Rendering (Layout = null;): By declaring Layout = null; at the absolute peak of the Razor canvas, you tell the view compiler to bypass your application's global headers, dark sidebars, and analytical footer code blocks. This isolation ensures the file outputs a standard, standalone document suitable for customer fulfillment.

  • Advanced CSS Print Media Queries (@media print): This section uses a built-in browser engine rule descriptor. When an operator triggers a print request via physical paper save setups or PDF generation engines, the browser switches style contexts dynamically:

    • .no-print { display: none !important; } completely strips away active controls like the "Print Invoice" and "Back to Order" action button blocks so they do not print onto paper.

    • -webkit-print-color-adjust: exact; overrides the native behavior of WebKit engines (like Google Chrome and Safari) that strips background styling, keeping your blue corporate totals layout bars (.total-row) and table heading lines crisp on paper.

  • Safe Conditional Breakdown Objects Model Data Mapping: The invoice sheet implements clean validation checks across your nested database properties. If an element like item.Product or the primary buyer relation object evaluates to null, the loop defaults to fallback text flags (Product #@item.ProductId or Guest Customer), ensuring the print document never crashes due to missing product data.

  • Automated URL Parameter Print Trigger Scripting:

    const urlParams = new URLSearchParams(window.location.search);
    if (urlParams.get('print') === 'true') { ... }
    

    The script includes an embedded native vanilla JavaScript automation listener. When a manager targets this route with a query string like /Admin/Orders/Invoice/45?print=true, the system captures that property value and triggers the browser's printing panel (window.print()) exactly 500 milliseconds after the view loads.

Comments