Food Ordering System Part 28 | Building an Advanced Admin Order Management Panel in ASP.NET MVC Core
In this tutorial, we are diving deep into the Admin Controller to manage customer orders efficiently. A robust Admin Panel is the backbone of any Food Ordering System, allowing staff to track, filter, and organize orders in real-time.
Key Features Explained:
Authorization & Security: The
IsAdmin()check ensures that only users with administrative privileges can access the order management data.Dynamic Sorting: Using
ViewBag, we toggle between ascending and descending orders for dates, totals, and statuses.Advanced Filtering:
Status Filter: View orders by their current stage (e.g., Pending, Delivered).
Search Functionality: Quickly find orders by searching for a Customer's Full Name, Username, or Email.
Date Range: A crucial feature for accounting, allowing admins to see orders between two specific dates.
Eager Loading: We use
.Include()and.ThenInclude()to pull related User and Food Item data in a single query, optimizing performance.Dashboard Statistics: We calculate real-time stats like
PendingCountandTotalRevenueto give the admin an instant overview of the business health.
public IActionResult Orders(string status, string searchString, DateTime? fromDate, DateTime? toDate, string sortOrder)
{
if (!IsAdmin()) return RedirectToAction("Index", "Home");
// Store current sort order for view
ViewBag.CurrentSort = sortOrder;
ViewBag.DateSortParam = sortOrder == "date_asc" ? "date_desc" : "date_asc";
ViewBag.TotalSortParam = sortOrder == "total_asc" ? "total_desc" : "total_asc";
ViewBag.StatusSortParam = sortOrder == "status" ? "status_desc" : "status";
var orders = _context.Orders
.Include(o => o.User)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.FoodItem)
.AsQueryable();
// Filter by Status
if (!string.IsNullOrEmpty(status) && status != "All")
{
orders = orders.Where(o => o.Status == status);
ViewBag.CurrentStatus = status;
}
// Filter by Customer Name
if (!string.IsNullOrEmpty(searchString))
{
orders = orders.Where(o =>
o.User.FullName.Contains(searchString) ||
o.User.Username.Contains(searchString) ||
o.User.Email.Contains(searchString));
ViewBag.CurrentSearch = searchString;
}
// Filter by Date Range
if (fromDate.HasValue)
{
orders = orders.Where(o => o.OrderDate >= fromDate.Value);
ViewBag.FromDate = fromDate.Value.ToString("yyyy-MM-dd");
}
if (toDate.HasValue)
{
orders = orders.Where(o => o.OrderDate <= toDate.Value.AddDays(1));
ViewBag.ToDate = toDate.Value.ToString("yyyy-MM-dd");
}
// Sorting
orders = sortOrder switch
{
"date_asc" => orders.OrderBy(o => o.OrderDate),
"date_desc" => orders.OrderByDescending(o => o.OrderDate),
"total_asc" => orders.OrderBy(o => o.TotalAmount),
"total_desc" => orders.OrderByDescending(o => o.TotalAmount),
"status" => orders.OrderBy(o => o.Status),
"status_desc" => orders.OrderByDescending(o => o.Status),
_ => orders.OrderByDescending(o => o.OrderDate) // default
};
// Status counts for dashboard stats
ViewBag.PendingCount = _context.Orders.Count(o => o.Status == "Pending");
ViewBag.ConfirmedCount = _context.Orders.Count(o => o.Status == "Confirmed");
ViewBag.TodayCount = _context.Orders.Count(o => o.OrderDate.Date == DateTime.Today);
ViewBag.TotalRevenue = _context.Orders.Where(o => o.Status != "Cancelled").Sum(o => (decimal?)o.TotalAmount) ?? 0;
ViewBag.StatusList = new List<string> { "All", "Pending", "Confirmed", "Preparing", "OutForDelivery", "Delivered", "Cancelled" };
return View(orders.ToList());
}
Code Explanation for Your Readers
In this session, we focus on the Front-end UI (Razor View) of the Admin Order Panel. This view is designed to give restaurant owners a 360-degree view of their business operations.
Key UI Components:
Dashboard Stats Cards: At a glance, the admin can see Pending Orders, Confirmed Orders, Today's Sales Volume, and Total Revenue.
Advanced Filtering Form: A clean horizontal form allows users to filter the order list by Status, Customer Name (Search), and Specific Date Ranges without leaving the page.
Active Filter Badges: If filters are applied, dynamic "badges" appear to show what is currently being viewed, along with a "Clear All" option for better UX.
Dynamic Table Sorting: The table headers (Date, Total, Status) are clickable, allowing the admin to toggle the sort order dynamically.
Conditional Styling: We implemented a C#
switchexpression inside the Razor view to automatically assign different Bootstrap colors to order statuses (e.g., Green for Delivered, Red for Cancelled).Print & Export: Integrated buttons for printing the order list or exporting data.
@model List<Order>
@{
ViewData["Title"] = "All Orders";
}
<div class="container-fluid my-4">
<h2 class="mb-4"><i class="bi bi-list-check"></i> Order Management</h2>
<!-- Stats Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-warning text-dark">
<div class="card-body d-flex justify-content-between">
<div>
<h6>Pending Orders</h6>
<h3>@ViewBag.PendingCount</h3>
</div>
<i class="bi bi-clock display-4"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body d-flex justify-content-between">
<div>
<h6>Confirmed</h6>
<h3>@ViewBag.ConfirmedCount</h3>
</div>
<i class="bi bi-check-circle display-4"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body d-flex justify-content-between">
<div>
<h6>Today's Orders</h6>
<h3>@ViewBag.TodayCount</h3>
</div>
<i class="bi bi-calendar-check display-4"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body d-flex justify-content-between">
<div>
<h6>Total Revenue</h6>
<h3>$@ViewBag.TotalRevenue.ToString("F0")</h3>
</div>
<i class="bi bi-cash-stack display-4"></i>
</div>
</div>
</div>
</div>
@if (TempData["Success"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@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">
@TempData["Error"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
<!-- Filter Section -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="bi bi-funnel"></i> Filter Orders</h5>
</div>
<div class="card-body">
<form method="get" asp-action="Orders" class="row g-3 align-items-end">
<div class="col-md-2">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="All">All Status</option>
@foreach (var s in ViewBag.StatusList)
{
if (s != "All")
{
<option value="@s" selected="@(ViewBag.CurrentStatus == s)">@s</option>
}
}
</select>
</div>
<div class="col-md-3">
<label class="form-label">Search Customer</label>
<input type="text" name="searchString" class="form-control"
value="@ViewBag.CurrentSearch" placeholder="Name, Username or Email..." />
</div>
<div class="col-md-2">
<label class="form-label">From Date</label>
<input type="date" name="fromDate" class="form-control" value="@ViewBag.FromDate" />
</div>
<div class="col-md-2">
<label class="form-label">To Date</label>
<input type="date" name="toDate" class="form-control" value="@ViewBag.ToDate" />
</div>
<div class="col-md-2">
<label class="form-label">Sort By</label>
<select name="sortOrder" class="form-select">
<option value="">Newest First</option>
<option value="date_asc" selected="@(ViewBag.CurrentSort == "date_asc")">Oldest First</option>
<option value="total_desc" selected="@(ViewBag.CurrentSort == "total_desc")">Highest Amount</option>
<option value="total_asc" selected="@(ViewBag.CurrentSort == "total_asc")">Lowest Amount</option>
<option value="status" selected="@(ViewBag.CurrentSort == "status")">Status (A-Z)</option>
</select>
</div>
<div class="col-md-1">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-search"></i>
</button>
</div>
</form>
@if (!string.IsNullOrEmpty(ViewBag.CurrentStatus) ||
!string.IsNullOrEmpty(ViewBag.CurrentSearch) ||
ViewBag.FromDate != null ||
ViewBag.ToDate != null)
{
<div class="mt-3">
<span class="text-muted">Active Filters:</span>
@if (!string.IsNullOrEmpty(ViewBag.CurrentStatus))
{
<span class="badge bg-primary me-1">Status: @ViewBag.CurrentStatus</span>
}
@if (!string.IsNullOrEmpty(ViewBag.CurrentSearch))
{
<span class="badge bg-secondary me-1">Search: @ViewBag.CurrentSearch</span>
}
@if (ViewBag.FromDate != null)
{
<span class="badge bg-info me-1">From: @ViewBag.FromDate</span>
}
@if (ViewBag.ToDate != null)
{
<span class="badge bg-info me-1">To: @ViewBag.ToDate</span>
}
<a href="/Admin/Orders" class="btn btn-sm btn-outline-danger ms-2">
<i class="bi bi-x-circle"></i> Clear All
</a>
</div>
}
</div>
</div>
<!-- Orders Table -->
<div class="card shadow">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Showing @Model.Count Orders</span>
<div>
<a href="#" class="btn btn-sm btn-success" onclick="window.print()">
<i class="bi bi-printer"></i> Print
</a>
<a href="#" class="btn btn-sm btn-outline-primary">
<i class="bi bi-download"></i> Export
</a>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover align-middle">
<thead class="table-dark">
<tr>
<th>Order ID</th>
<th>
<a href="@Url.Action("Orders", new { sortOrder = ViewBag.DateSortParam, status = ViewBag.CurrentStatus, searchString = ViewBag.CurrentSearch, fromDate = ViewBag.FromDate, toDate = ViewBag.ToDate })" class="text-white text-decoration-none">
Date <i class="bi bi-arrow-down-up"></i>
</a>
</th>
<th>Customer</th>
<th>Contact</th>
<th>
<a href="@Url.Action("Orders", new { sortOrder = ViewBag.TotalSortParam, status = ViewBag.CurrentStatus, searchString = ViewBag.CurrentSearch, fromDate = ViewBag.FromDate, toDate = ViewBag.ToDate })" class="text-white text-decoration-none">
Total <i class="bi bi-arrow-down-up"></i>
</a>
</th>
<th>
<a href="@Url.Action("Orders", new { sortOrder = ViewBag.StatusSortParam, status = ViewBag.CurrentStatus, searchString = ViewBag.CurrentSearch, fromDate = ViewBag.FromDate, toDate = ViewBag.ToDate })" class="text-white text-decoration-none">
Status <i class="bi bi-arrow-down-up"></i>
</a>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var order in Model)
{
<tr>
<td><strong>#@order.Id</strong></td>
<td>@order.OrderDate.ToString("MM/dd/yyyy HH:mm")</td>
<td>
<div>@order.User?.FullName</div>
<small class="text-muted">@order.User?.Username</small>
</td>
<td>@order.PhoneNumber</td>
<td class="fw-bold">$@order.TotalAmount.ToString("F2")</td>
<td>
@{
var badgeClass = order.Status switch
{
"Pending" => "bg-warning text-dark",
"Confirmed" => "bg-info",
"Preparing" => "bg-primary",
"OutForDelivery" => "bg-secondary",
"Delivered" => "bg-success",
"Cancelled" => "bg-danger",
_ => "bg-light text-dark"
};
}
<span class="badge @badgeClass">@order.Status</span>
</td>
<td>
<div class="btn-group" role="group">
<a href="#" class="btn btn-sm btn-outline-info" title="View Details">
<i class="bi bi-eye"></i>
</a>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
@if (!Model.Any())
{
<div class="text-center py-5 text-muted">
<i class="bi bi-search display-4"></i>
<p class="mt-3">No orders found matching your criteria.</p>
<a href="/Admin/Orders" class="btn btn-outline-primary">Clear Filters</a>
</div>
}
</div>
</div>
</div>

Comments
Post a Comment