Food Ordering System Part 20 | Real-Time Order Tracking Page in ASP.NET Core MVC


In Part 20, we are moving from the high-level dashboard to the detailed Track Order feature. This backend logic is the "security guard" of your application, ensuring that when a user clicks "Track," they see exactly what they ordered—and no one else's data.


Step-by-Step Code Explanation

  1. The ID Parameter: The method accepts an int id. This is the unique Order ID passed from the "My Orders" list, allowing us to find one specific transaction.

  2. Session Security: Just like in the previous parts, we check HttpContext.Session.GetInt32("UserId"). This prevents unauthenticated users from accessing the tracking page.

  3. Deep Data Retrieval (Include):

    • We use .Include(o => o.OrderItems) and .ThenInclude(oi => oi.FoodItem) to pull the entire "family tree" of the order. This ensures the tracking page can show the names and prices of the food items being tracked.

  4. Double-Lock Security:

    • The .FirstOrDefault(o => o.Id == id && o.UserId == userId) line is critical.

    • It doesn't just look for the Order ID; it checks that the order belongs to the person currently logged in. This prevents a "hack" where a user manually types a different ID in the URL to spy on other people's orders.

  5. Null Handling: If the order doesn't exist or doesn't belong to the user, we return NotFound(), which is much cleaner and more professional than letting the app crash.


 

ORDERCONTROLLER.CS (PART 20)
public IActionResult TrackOrder(int id)
{
    var userId = HttpContext.Session.GetInt32("UserId");
    if (userId == null) return RedirectToAction("Login", "Account");

    var order = _context.Orders
        .Include(o => o.OrderItems)
        .ThenInclude(oi => oi.FoodItem)
        .FirstOrDefault(o => o.Id == id && o.UserId == userId);

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

    return View(order);
}

This Track Order View is the visual heart of our delivery system. It takes the raw data from the database and transforms it into a professional, user-friendly timeline. By using Bootstrap’s progress bars and icons, we give the customer a "real-time" feel for their order's journey.


Step-by-Step Code Explanation

  1. Dynamic Status Array: We define a statuses array that matches the order flow: Pending → Confirmed → Preparing → OutForDelivery → Delivered.

  2. The Progress Bar Logic:

    • We find the currentIndex of the order's current status in our array.

    • The width of the green progress bar is calculated mathematically: @((currentIndex * 25))%. This ensures the bar grows perfectly as the order moves through the five stages.

  3. Visual Timeline: We loop through the statuses using a @for loop. If a step is "active" (meaning the order has reached or passed that stage), we highlight it in Success Green using a conditional class: isActive ? "bg-success text-white" : "bg-light text-muted".

  4. Icon Switching: A C# switch expression assigns a unique Bootstrap icon to each stage (e.g., a "fire" for Preparing or a "truck" for Out for Delivery).

  5. Cancellation Handling: If the status is "Cancelled," the logic intelligently hides the progress bar and displays a clear, red alert box instead.

  6. Summary Table: At the bottom, we list every item in the order with its image and subtotal, concluding with a bolded final total.


VIEWS/ORDER/TRACKORDER.CSHTML
@model Order
@{
    ViewData["Title"] = $"Track Order #{Model.Id}";
}

<div class="container my-5">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card shadow">
                <div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
                    <h4 class="mb-0">Order #@Model.Id</h4>
                    <span class="badge bg-light text-dark">@Model.Status</span>
                </div>
                <div class="card-body">
                    <!-- Progress Tracker -->
                    <div class="progress-tracker mb-5">
                        @{
                            var statuses = new[] { "Pending", "Confirmed", "Preparing", "OutForDelivery", "Delivered" };
                            var currentIndex = Array.IndexOf(statuses, Model.Status);
                            if (Model.Status == "Cancelled")
                            {
                                <div class="alert alert-danger text-center">
                                    <i class="bi bi-x-octagon-fill display-4"></i>
                                    <h4 class="mt-2">Order Cancelled</h4>
                                    <p>This order has been cancelled.</p>
                                </div>
                            }
                            else
                            {
                                <div class="d-flex justify-content-between position-relative mb-3">
                                    <div class="progress position-absolute w-100" style="height: 5px; top: 20px; z-index: 0;">
                                        <div class="progress-bar bg-success" role="progressbar" style="width: @((currentIndex * 25))%"></div>
                                    </div>

                                    @for (int i = 0; i < statuses.Length; i++)
                                    {
                                        var isActive = i <= currentIndex;
                                        var isCurrent = i == currentIndex;
                                        var icon = statuses[i] switch
                                        {
                                            "Pending" => "clock",
                                            "Confirmed" => "check-circle",
                                            "Preparing" => "fire",
                                            "OutForDelivery" => "truck",
                                            "Delivered" => "box-seam",
                                            _ => "circle"
                                        };

                                        <div class="text-center position-relative" style="z-index: 1;">
                                            <div class="rounded-circle d-flex align-items-center justify-content-center mx-auto mb-2
                                                         @(isActive ? "bg-success text-white" : "bg-light text-muted")"
                                                 style="width: 45px; height: 45px; border: 3px solid @(isActive ? "#28a745" : "#dee2e6");">
                                                <i class="bi bi-@icon fs-5"></i>
                                            </div>
                                            <small class="@(isActive ? "text-success fw-bold" : "text-muted")">
                                                @(statuses[i] == "OutForDelivery" ? "Out for Delivery" : statuses[i])
                                            </small>
                                            @if (isCurrent && Model.Status != "Delivered")
                                            {
                                                <br />
                                                <span class="badge bg-primary mt-1">Current</span>
                                            }
                                        </div>
                                    }
                                </div>
                            }
                        }
                    </div>

                    <hr />

                    <!-- Order Details -->
                    <div class="row mb-4">
                        <div class="col-md-6">
                            <h6 class="text-muted">Order Date</h6>
                            <p>@Model.OrderDate.ToString("MMMM dd, yyyy at HH:mm")</p>
                        </div>
                        <div class="col-md-6">
                            <h6 class="text-muted">Estimated Delivery</h6>
                            <p>
                                @if (Model.Status == "Delivered")
                                {
                                    <span class="text-success">Delivered on @Model.OrderDate.AddHours(1).ToString("MMM dd, HH:mm")</span>
                                }
                                else if (Model.Status == "Cancelled")
                                {
                                    <span class="text-danger">Order Cancelled</span>
                                }
                                else
                                {
                                    <span>30-45 minutes</span>
                                }
                            </p>
                        </div>
                    </div>

                    <div class="row mb-4">
                        <div class="col-md-6">
                            <h6 class="text-muted">Delivery Address</h6>
                            <p>@Model.DeliveryAddress</p>
                        </div>
                        <div class="col-md-6">
                            <h6 class="text-muted">Contact Number</h6>
                            <p>@Model.PhoneNumber</p>
                        </div>
                    </div>

                    <!-- Items -->
                    <h6 class="mb-3">Order Items</h6>
                    <table class="table table-sm">
                        <tbody>
                            @foreach (var item in Model.OrderItems)
                            {
                                <tr>
                                    <td>
                                        <img src="@item.FoodItem?.ImageUrl" alt="@item.FoodItem?.Name"
                                             style="width: 50px; height: 50px; object-fit: cover;" class="rounded" />
                                    </td>
                                    <td>@item.FoodItem?.Name</td>
                                    <td>x @item.Quantity</td>
                                    <td class="text-end">$@((item.Quantity * item.UnitPrice).ToString("F2"))</td>
                                </tr>
                            }
                        </tbody>
                        <tfoot>
                            <tr class="table-active">
                                <td colspan="3" class="text-end"><strong>Total:</strong></td>
                                <td class="text-end"><strong>$@Model.TotalAmount.ToString("F2")</strong></td>
                            </tr>
                        </tfoot>
                    </table>

                    <div class="d-flex justify-content-between mt-4">
                        <a href="/Order/MyOrders" class="btn btn-outline-secondary">
                            <i class="bi bi-arrow-left"></i> Back to My Orders
                        </a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

This "Track Order" button is the final bridge between the My Orders list and the detailed tracking page we built in Part 20. It is placed within the loop of your order cards, allowing users to jump directly into the live status of any specific meal.


Step-by-Step Code Explanation

  • d-grid gap-2: This Bootstrap utility class makes the button "block-level," meaning it will stretch to fill the entire width of the card. This is a modern mobile-first design practice that makes the button much easier to tap on smartphones.

  • Dynamic Routing (@order.Id): The href attribute uses C# Razor syntax to append the unique ID of the order to the URL. For example, if you click the button for Order #105, it correctly points the browser to /Order/TrackOrder/105.

  • btn-outline-primary: We use the "outline" version of the Bootstrap button here. This keeps the UI looking clean and less cluttered, especially when there are multiple order cards on one screen.

  • bi-geo-alt: This adds a "location pin" icon from the Bootstrap Icons library, which visually communicates to the user that they are about to see a tracking/location-based feature.


MYORDERS.CSHTML (BUTTON SNIPPET)
<div class="card-body">
    <div class="d-grid gap-2">
        <a href="/Order/TrackOrder/@order.Id" class="btn btn-outline-primary">
            <i class="bi bi-geo-alt"></i> Track Order
        </a>
    </div>
</div>

Comments