Food Ordering System Part 23: How to Manage Food Items & Prices in ASP.NET Core MVC


 In Part 23, we are building a high-performance Menu Management System. This ManageMenu method is significantly more advanced than a standard list; it features a full suite of filters and a dynamic sorting engine. This allows the Admin to quickly find specific dishes, check inventory, and manage pricing across hundreds of items.


Step-by-Step Code Explanation

  1. Security & Sort Setup: After the IsAdmin() check, the code sets up ViewBag parameters for sorting. It uses a "toggle" logic—if the current sort is "name", clicking it again will change it to "name_desc".

  2. AsQueryable() for Performance: We start with _context.FoodItems.AsQueryable(). This is crucial because it allows us to build the database query in stages without actually fetching data until the very end.

  3. Advanced Filtering:

    • Category: Filters items belonging to a specific menu section.

    • Search: Checks both the Name and Description for matches using .Contains().

    • Availability: Filters by whether the item is currently "In Stock" or "Out of Stock."

    • Price Range: Implements a minPrice and maxPrice logic to find items within a specific budget.

  4. The Sorting Engine: Using a C# switch expression, the code reorders the list based on the user's choice (Name, Price, or Category). The _ (underscore) acts as a default fallback, sorting by Category and then Name.

  5. Admin Statistics: Before sending the data to the view, the code calculates quick stats (Total, Available, and Unavailable items) to give the Admin a "snapshot" of the current menu status.

  6. Final Execution: Finally, items.ToList() is called. This is the moment the fully filtered and sorted query is executed against the SQL database.


ADMINCONTROLLER.CS (MANAGEMENU)
public IActionResult ManageMenu(int? categoryId, string searchString, bool? isAvailable, decimal? minPrice, decimal? maxPrice, string sortOrder)
{
    if (!IsAdmin()) return RedirectToAction("Index", "Home");

    // Store sort order for view
    ViewBag.CurrentSort = sortOrder;
    ViewBag.NameSortParam = sortOrder == "name_desc" ? "name" : "name_desc";
    ViewBag.PriceSortParam = sortOrder == "price_asc" ? "price_desc" : "price_asc";
    ViewBag.CategorySortParam = sortOrder == "category" ? "category_desc" : "category";

    var items = _context.FoodItems
        .Include(f => f.Category)
        .AsQueryable();

    // Filter by Category
    if (categoryId.HasValue && categoryId > 0)
    {
        items = items.Where(f => f.CategoryId == categoryId);
        ViewBag.CurrentCategory = categoryId;
    }

    // Filter by Search String (Name or Description)
    if (!string.IsNullOrEmpty(searchString))
    {
        items = items.Where(f =>
            f.Name.Contains(searchString) ||
            f.Description.Contains(searchString));
        ViewBag.CurrentSearch = searchString;
    }

    // Filter by Availability
    if (isAvailable.HasValue)
    {
        items = items.Where(f => f.IsAvailable == isAvailable.Value);
        ViewBag.CurrentAvailability = isAvailable.Value;
    }

    // Filter by Price Range
    if (minPrice.HasValue)
    {
        items = items.Where(f => f.Price >= minPrice.Value);
        ViewBag.MinPrice = minPrice.Value;
    }
    if (maxPrice.HasValue)
    {
        items = items.Where(f => f.Price <= maxPrice.Value);
        ViewBag.MaxPrice = maxPrice.Value;
    }

    // Sorting
    items = sortOrder switch
    {
        "name_asc" => items.OrderBy(f => f.Name),
        "name_desc" => items.OrderByDescending(f => f.Name),
        "price_asc" => items.OrderBy(f => f.Price),
        "price_desc" => items.OrderByDescending(f => f.Price),
        "category" => items.OrderBy(f => f.Category.Name),
        "category_desc" => items.OrderByDescending(f => f.Category.Name),
        _ => items.OrderBy(f => f.CategoryId).ThenBy(f => f.Name)
    };

    // Stats for dashboard
    ViewBag.TotalItems = _context.FoodItems.Count();
    ViewBag.AvailableItems = _context.FoodItems.Count(f => f.IsAvailable);
    ViewBag.UnavailableItems = _context.FoodItems.Count(f => !f.IsAvailable);
    ViewBag.CategoryCount = _context.Categories.Count();

    ViewBag.Categories = _context.Categories.ToList();

    return View(items.ToList());
}

In Part 23, we are creating a professional-grade Menu Management View. This UI isn't just a simple list; it’s a full administrative dashboard featuring real-time statistics, a multi-parameter filtering system, and a secure deletion workflow using Bootstrap Modals.


Step-by-Step Code Explanation

  1. Summary Stats: At the top, we use four color-coded cards to display the current state of the menu. This gives the Admin instant insight into total inventory and availability without scrolling through the table.

  2. Advanced Filter Form: This section uses a GET method to send search parameters (Category, Search String, Availability, and Price Range) back to our ManageMenu action. It also includes "Active Filter" badges that show the user exactly what criteria are currently applied.

  3. Dynamic Sorting Links: The table headers are clickable. By passing the sortOrder back to the controller, the Admin can toggle between ascending and descending order for Names, Prices, and Categories.

  4. The Data Table:

    • Image Handling: Includes an onerror fallback to a placeholder image, ensuring the UI stays clean even if a link is broken.

    • Status Badges: Uses green/red badges to visually highlight stock status.

    • Responsive Wrapper: The table-responsive class ensures the table remains usable on mobile and tablet devices.

  5. Delete Confirmation Logic: To prevent accidental data loss, the "Delete" button doesn't trigger a delete immediately. It calls a JavaScript function confirmDelete() that opens a Bootstrap Modal.

  6. Modal & Script: The script dynamically injects the specific Item ID and Name into the modal's hidden form, ensuring the correct item is deleted only after the Admin clicks the final confirmation button.


VIEWS/ADMIN/MANAGEMENU.CSHTML
@model List<FoodItem>
@{
    ViewData["Title"] = "Manage Menu";
}

<div class="container-fluid my-4">
    <h2 class="mb-4"><i class="bi bi-menu-up"></i> Menu Management</h2>

    <!-- Stats Cards -->
    <div class="row mb-4">
        <div class="col-md-3">
            <div class="card bg-primary text-white">
                <div class="card-body d-flex justify-content-between">
                    <div>
                        <h6>Total Items</h6>
                        <h3>@ViewBag.TotalItems</h3>
                    </div>
                    <i class="bi bi-box-seam 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>Available</h6>
                        <h3>@ViewBag.AvailableItems</h3>
                    </div>
                    <i class="bi bi-check-circle display-4"></i>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card bg-warning text-dark">
                <div class="card-body d-flex justify-content-between">
                    <div>
                        <h6>Unavailable</h6>
                        <h3>@ViewBag.UnavailableItems</h3>
                    </div>
                    <i class="bi bi-x-circle 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>Categories</h6>
                        <h3>@ViewBag.CategoryCount</h3>
                    </div>
                    <i class="bi bi-tags 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>
    }

    <!-- 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 Menu Items</h5>
        </div>
        <div class="card-body">
            <form method="get" asp-action="ManageMenu" class="row g-3 align-items-end">
                <div class="col-md-2">
                    <label class="form-label">Category</label>
                    <select name="categoryId" class="form-select">
                        <option value="0">All Categories</option>
                        @foreach (var cat in ViewBag.Categories)
                        {
                            <option value="@cat.Id" selected="@(ViewBag.CurrentCategory == cat.Id)">@cat.Name</option>
                        }
                    </select>
                </div>
                <div class="col-md-3">
                    <label class="form-label">Search</label>
                    <input type="text" name="searchString" class="form-control" 
                           value="@ViewBag.CurrentSearch" placeholder="Name or description..." />
                </div>
                <div class="col-md-2">
                    <label class="form-label">Availability</label>
                    <select name="isAvailable" class="form-select">
                        <option value="">All</option>
                        <option value="true" selected="@(ViewBag.CurrentAvailability == true)">Available Only</option>
                        <option value="false" selected="@(ViewBag.CurrentAvailability == false)">Unavailable Only</option>
                    </select>
                </div>
                <div class="col-md-2">
                    <label class="form-label">Min Price ($)</label>
                    <input type="number" name="minPrice" class="form-control" step="0.01" 
                           value="@ViewBag.MinPrice" placeholder="0.00" />
                </div>
                <div class="col-md-2">
                    <label class="form-label">Max Price ($)</label>
                    <input type="number" name="maxPrice" class="form-control" step="0.01" 
                           value="@ViewBag.MaxPrice" placeholder="Max" />
                </div>
                <div class="col-md-1">
                    <button type="submit" class="btn btn-primary w-100">
                        <i class="bi bi-search"></i>
                    </button>
                </div>
            </form>
            
            <div class="row mt-3">
                <div class="col-md-6">
                    @if (ViewBag.CurrentCategory != null || 
                         !string.IsNullOrEmpty(ViewBag.CurrentSearch) || 
                         ViewBag.CurrentAvailability != null ||
                         ViewBag.MinPrice != null || 
                         ViewBag.MaxPrice != null)
                    {
                        <span class="text-muted">Active Filters:</span>
                        @if (!string.IsNullOrEmpty(ViewBag.CurrentSearch))
                        {
                            <span class="badge bg-secondary me-1">Search: @ViewBag.CurrentSearch</span>
                        }
                        @if (ViewBag.CurrentAvailability != null)
                        {
                            <span class="badge bg-info me-1">@(ViewBag.CurrentAvailability == true ? "Available" : "Unavailable")</span>
                        }
                        @if (ViewBag.MinPrice != null)
                        {
                            <span class="badge bg-warning me-1">Min: $@ViewBag.MinPrice</span>
                        }
                        @if (ViewBag.MaxPrice != null)
                        {
                            <span class="badge bg-warning me-1">Max: $@ViewBag.MaxPrice</span>
                        }
                        <a href="/Admin/ManageMenu" class="btn btn-sm btn-outline-danger ms-2">
                            <i class="bi bi-x-circle"></i> Clear
                        </a>
                    }
                </div>
                <div class="col-md-6 text-end">
                    <a href="/Admin/AddFoodItem" class="btn btn-success">
                        <i class="bi bi-plus-circle"></i> Add New Item
                    </a>
                </div>
            </div>
        </div>
    </div>

    <!-- Items Table -->
    <div class="card shadow">
        <div class="card-header d-flex justify-content-between align-items-center">
            <span>Showing @Model.Count Items</span>
            <div class="btn-group">
                <a href="@Url.Action("ManageMenu", new { 
                    categoryId = ViewBag.CurrentCategory, 
                    searchString = ViewBag.CurrentSearch,
                    isAvailable = ViewBag.CurrentAvailability,
                    minPrice = ViewBag.MinPrice,
                    maxPrice = ViewBag.MaxPrice,
                    sortOrder = ViewBag.NameSortParam 
                })" class="btn btn-sm btn-outline-secondary">
                    Name <i class="bi bi-arrow-down-up"></i>
                </a>
                <a href="@Url.Action("ManageMenu", new { 
                    categoryId = ViewBag.CurrentCategory, 
                    searchString = ViewBag.CurrentSearch,
                    isAvailable = ViewBag.CurrentAvailability,
                    minPrice = ViewBag.MinPrice,
                    maxPrice = ViewBag.MaxPrice,
                    sortOrder = ViewBag.PriceSortParam 
                })" class="btn btn-sm btn-outline-secondary">
                    Price <i class="bi bi-arrow-down-up"></i>
                </a>
                <a href="@Url.Action("ManageMenu", new { 
                    categoryId = ViewBag.CurrentCategory, 
                    searchString = ViewBag.CurrentSearch,
                    isAvailable = ViewBag.CurrentAvailability,
                    minPrice = ViewBag.MinPrice,
                    maxPrice = ViewBag.MaxPrice,
                    sortOrder = ViewBag.CategorySortParam 
                })" class="btn btn-sm btn-outline-secondary">
                    Category <i class="bi bi-arrow-down-up"></i>
                </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>Image</th>
                            <th>Name</th>
                            <th>Category</th>
                            <th>Price</th>
                            <th>Status</th>
                            <th>Actions</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach (var item in Model)
                        {
                            <tr>
                                <td>
                                    <img src="@item.ImageUrl" alt="@item.Name" 
                                         style="width: 60px; height: 60px; object-fit: cover;" 
                                         class="rounded" 
                                         onerror="this.src='/images/placeholder-food.jpg'" />
                                </td>
                                <td>
                                    <strong>@item.Name</strong>
                                    <br />
                                    <small class="text-muted">@item.Description</small>
                                </td>
                                <td>
                                    <span class="badge bg-secondary">@item.Category?.Name</span>
                                </td>
                                <td class="fw-bold text-primary">$@item.Price.ToString("F2")</td>
                                <td>
                                    @if (item.IsAvailable)
                                    {
                                        <span class="badge bg-success"><i class="bi bi-check-circle"></i> Available</span>
                                    }
                                    else
                                    {
                                        <span class="badge bg-danger"><i class="bi bi-x-circle"></i> Unavailable</span>
                                    }
                                </td>
                                <td>
                                    <button type="button" class="btn btn-sm btn-danger" 
                                            onclick="confirmDelete(@item.Id, '@item.Name')">
                                        <i class="bi bi-trash"></i>
                                    </button>
                                </td>
                            </tr>
                        }
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>

<!-- Delete Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header bg-danger text-white">
                <h5 class="modal-title">Delete Menu Item</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <p>Are you sure you want to delete <strong id="deleteItemName"></strong>?</p>
            </div>
            <div class="modal-footer">
                <form id="deleteForm" method="post" asp-action="DeleteFoodItem">
                    <input type="hidden" name="id" id="deleteItemId" />
                    <button type="submit" class="btn btn-danger">Delete</button>
                </form>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <script>
        function confirmDelete(id, name) {
            document.getElementById('deleteItemId').value = id;
            document.getElementById('deleteItemName').textContent = name;
            var modal = new bootstrap.Modal(document.getElementById('deleteModal'));
            modal.show();
        }
    </script>
}

Comments