littleshop/LittleShop/Areas/Admin/Views/Orders/Index.cshtml
SysAdmin 2ae44a3c56 Fix: Add Dispatch button for orders in Packing status
- Added quick action button on Packing tab to dispatch orders
- Created dispatch modal with tracking number input
- Modal includes tracking number, estimated days, and notes fields
- Button appears on both desktop table and mobile card views
- Fixes workflow gap where Packing orders had no quick action
- Orders now properly flow: Accepted → Packing → Dispatched

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 16:12:11 +01:00

473 lines
27 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@{
ViewData["Title"] = "Order Management";
var orders = ViewData["Orders"] as IEnumerable<LittleShop.DTOs.OrderDto> ?? new List<LittleShop.DTOs.OrderDto>();
var currentTab = ViewData["CurrentTab"] as string ?? "accept";
var tabTitle = ViewData["TabTitle"] as string ?? "Orders";
var pendingCount = (int)(ViewData["PendingCount"] ?? 0);
var acceptCount = (int)(ViewData["AcceptCount"] ?? 0);
var packingCount = (int)(ViewData["PackingCount"] ?? 0);
var dispatchedCount = (int)(ViewData["DispatchedCount"] ?? 0);
var onHoldCount = (int)(ViewData["OnHoldCount"] ?? 0);
}
<div class="row mb-3">
<div class="col">
<h1 class="h3"><i class="fas fa-clipboard-list"></i> <span class="d-none d-md-inline">Order Management</span><span class="d-md-none">Orders</span></h1>
<p class="text-muted d-none d-md-block">Workflow-focused order fulfillment system</p>
</div>
<div class="col-auto">
<a href="@Url.Action("Create")" class="btn btn-primary btn-sm">
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Create Order</span><span class="d-sm-none">New</span>
</a>
</div>
</div>
<!-- Workflow Tabs - Mobile Responsive -->
<ul class="nav nav-tabs mb-3 flex-nowrap overflow-auto" id="orderTabs" role="tablist" style="white-space: nowrap;">
<li class="nav-item" role="presentation">
<a class="nav-link @(currentTab == "pending" ? "active" : "")" href="@Url.Action("Index", new { tab = "pending" })">
<i class="fas fa-clock"></i>
<span class="d-none d-md-inline">Pending Payment</span>
<span class="d-md-none">Pending</span>
@if (pendingCount > 0)
{
<span class="badge bg-secondary ms-1">@pendingCount</span>
}
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link @(currentTab == "accept" ? "active" : "")" href="@Url.Action("Index", new { tab = "accept" })">
<i class="fas fa-check-circle"></i>
<span class="d-none d-md-inline">Accept Orders</span>
<span class="d-md-none">Accept</span>
@if (acceptCount > 0)
{
<span class="badge bg-danger ms-1">@acceptCount</span>
}
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link @(currentTab == "packing" ? "active" : "")" href="@Url.Action("Index", new { tab = "packing" })">
<i class="fas fa-box"></i>
<span class="d-none d-md-inline">Packing</span>
<span class="d-md-none">Pack</span>
@if (packingCount > 0)
{
<span class="badge bg-warning ms-1">@packingCount</span>
}
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link @(currentTab == "dispatched" ? "active" : "")" href="@Url.Action("Index", new { tab = "dispatched" })">
<i class="fas fa-shipping-fast"></i>
<span class="d-none d-md-inline">Dispatched</span>
<span class="d-md-none">Ship</span>
@if (dispatchedCount > 0)
{
<span class="badge bg-info ms-1">@dispatchedCount</span>
}
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link @(currentTab == "delivered" ? "active" : "")" href="@Url.Action("Index", new { tab = "delivered" })">
<i class="fas fa-check"></i>
<span class="d-none d-md-inline">Delivered</span>
<span class="d-md-none">Done</span>
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link @(currentTab == "onhold" ? "active" : "")" href="@Url.Action("Index", new { tab = "onhold" })">
<i class="fas fa-pause-circle"></i>
<span class="d-none d-md-inline">On Hold</span>
<span class="d-md-none">Hold</span>
@if (onHoldCount > 0)
{
<span class="badge bg-secondary ms-1">@onHoldCount</span>
}
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link @(currentTab == "cancelled" ? "active" : "")" href="@Url.Action("Index", new { tab = "cancelled" })">
<i class="fas fa-times-circle"></i>
<span class="d-none d-md-inline">Cancelled</span>
<span class="d-md-none">Cancel</span>
</a>
</li>
</ul>
<div class="card">
<div class="card-header">
<h5 class="mb-0">@tabTitle (@orders.Count())</h5>
</div>
<div class="card-body">
@if (orders.Any())
{
<!-- Desktop Table View (hidden on mobile) -->
<div class="table-responsive d-none d-lg-block">
<table class="table table-hover">
<thead>
<tr>
<th>Order ID</th>
<th>Customer</th>
<th>Items</th>
<th>Total</th>
<th>Status</th>
<th>Timeline</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var order in orders)
{
<tr>
<td>
<strong>#@order.Id.ToString().Substring(0, 8)</strong>
<br><small class="text-muted">@order.CreatedAt.ToString("MMM dd, HH:mm")</small>
</td>
<td>
@if (order.Customer != null)
{
<strong>@order.Customer.DisplayName</strong>
<br><small class="text-muted">@order.Customer.CustomerType</small>
}
else
{
<strong>@order.ShippingName</strong>
<br><small class="text-muted">Anonymous</small>
}
</td>
<td>
@foreach (var item in order.Items.Take(2))
{
<div>@item.Quantity× @item.ProductName</div>
@if (!string.IsNullOrEmpty(item.ProductMultiBuyName))
{
<small class="text-muted">(@item.ProductMultiBuyName)</small>
}
}
@if (order.Items.Count > 2)
{
<small class="text-muted">+@(order.Items.Count - 2) more...</small>
}
</td>
<td>
<strong>£@order.TotalAmount</strong>
<br><small class="text-muted">@order.Currency</small>
</td>
<td>
@{
var statusClass = order.Status switch
{
LittleShop.Enums.OrderStatus.PendingPayment => "bg-warning",
LittleShop.Enums.OrderStatus.PaymentReceived => "bg-info",
LittleShop.Enums.OrderStatus.Accepted => "bg-primary",
LittleShop.Enums.OrderStatus.Packing => "bg-warning",
LittleShop.Enums.OrderStatus.Dispatched => "bg-info",
LittleShop.Enums.OrderStatus.Delivered => "bg-success",
LittleShop.Enums.OrderStatus.OnHold => "bg-secondary",
LittleShop.Enums.OrderStatus.Cancelled => "bg-danger",
_ => "bg-light"
};
}
<span class="badge @statusClass">@order.Status</span>
@if (!string.IsNullOrEmpty(order.TrackingNumber))
{
<br><small class="text-muted">@order.TrackingNumber</small>
}
</td>
<td>
<small>
@if (order.AcceptedAt.HasValue)
{
<div>✅ Accepted @order.AcceptedAt.Value.ToString("MMM dd, HH:mm")</div>
}
@if (order.PackingStartedAt.HasValue)
{
<div>📦 Packing @order.PackingStartedAt.Value.ToString("MMM dd, HH:mm")</div>
}
@if (order.DispatchedAt.HasValue)
{
<div>🚚 Dispatched @order.DispatchedAt.Value.ToString("MMM dd, HH:mm")</div>
}
@if (order.ExpectedDeliveryDate.HasValue)
{
<div class="text-muted">📅 Expected @order.ExpectedDeliveryDate.Value.ToString("MMM dd")</div>
}
@if (order.OnHoldAt.HasValue)
{
<div class="text-warning">⏸️ On Hold: @order.OnHoldReason</div>
}
</small>
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="@Url.Action("Details", new { id = order.Id })" class="btn btn-outline-primary" title="View Details">
<i class="fas fa-eye"></i>
</a>
@* Workflow-specific actions *@
@if (order.Status == LittleShop.Enums.OrderStatus.PaymentReceived)
{
<form method="post" action="@Url.Action("AcceptOrder", new { id = order.Id })" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-success btn-sm" title="Accept Order">
<i class="fas fa-check"></i>
</button>
</form>
}
@if (order.Status == LittleShop.Enums.OrderStatus.Accepted)
{
<form method="post" action="@Url.Action("StartPacking", new { id = order.Id })" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-warning btn-sm" title="Start Packing">
<i class="fas fa-box"></i>
</button>
</form>
}
@if (order.Status == LittleShop.Enums.OrderStatus.Packing)
{
<button type="button" class="btn btn-info btn-sm" title="Dispatch Order" data-bs-toggle="modal" data-bs-target="#dispatchModal-@order.Id">
<i class="fas fa-shipping-fast"></i>
</button>
}
@if (order.Status != LittleShop.Enums.OrderStatus.OnHold && order.Status != LittleShop.Enums.OrderStatus.Delivered && order.Status != LittleShop.Enums.OrderStatus.Cancelled)
{
<button type="button" class="btn btn-secondary btn-sm" title="Put On Hold" data-bs-toggle="modal" data-bs-target="#holdModal-@order.Id">
<i class="fas fa-pause"></i>
</button>
}
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Mobile Card View (hidden on desktop) -->
<div class="d-lg-none">
@foreach (var order in orders)
{
<div class="card mb-3 border-start border-3 @(order.Status switch {
LittleShop.Enums.OrderStatus.PaymentReceived => "border-warning",
LittleShop.Enums.OrderStatus.Accepted => "border-primary",
LittleShop.Enums.OrderStatus.Packing => "border-info",
LittleShop.Enums.OrderStatus.Dispatched => "border-success",
LittleShop.Enums.OrderStatus.OnHold => "border-secondary",
_ => "border-light"
})">
<div class="card-body">
<div class="row align-items-center">
<div class="col">
<h6 class="card-title mb-1">
<strong>#@order.Id.ToString().Substring(0, 8)</strong>
<span class="badge @(order.Status switch {
LittleShop.Enums.OrderStatus.PendingPayment => "bg-warning",
LittleShop.Enums.OrderStatus.PaymentReceived => "bg-info",
LittleShop.Enums.OrderStatus.Accepted => "bg-primary",
LittleShop.Enums.OrderStatus.Packing => "bg-warning",
LittleShop.Enums.OrderStatus.Dispatched => "bg-info",
LittleShop.Enums.OrderStatus.Delivered => "bg-success",
LittleShop.Enums.OrderStatus.OnHold => "bg-secondary",
LittleShop.Enums.OrderStatus.Cancelled => "bg-danger",
_ => "bg-light"
}) ms-2">@order.Status</span>
</h6>
<div class="small text-muted mb-2">
@if (order.Customer != null)
{
<text><strong>@order.Customer.DisplayName</strong> - @order.Customer.CustomerType</text>
}
else
{
<text><strong>@order.ShippingName</strong> - Anonymous</text>
}
</div>
<div class="small mb-2">
<strong>£@order.TotalAmount</strong>
@if (order.Items.Any())
{
var firstItem = order.Items.First();
<text> - @firstItem.Quantity x @firstItem.ProductName</text>
@if (!string.IsNullOrEmpty(firstItem.ProductMultiBuyName))
{
<span class="text-muted">(@firstItem.ProductMultiBuyName)</span>
}
@if (order.Items.Count > 1)
{
<span class="text-muted"> +@(order.Items.Count - 1) more</span>
}
}
</div>
@if (!string.IsNullOrEmpty(order.TrackingNumber))
{
<div class="small text-muted">
📦 @order.TrackingNumber
</div>
}
<!-- Timeline for mobile -->
<div class="small text-muted mt-2">
@if (order.AcceptedAt.HasValue)
{
<div>✅ @order.AcceptedAt.Value.ToString("MMM dd, HH:mm")</div>
}
@if (order.PackingStartedAt.HasValue)
{
<div>📦 @order.PackingStartedAt.Value.ToString("MMM dd, HH:mm")</div>
}
@if (order.DispatchedAt.HasValue)
{
<div>🚚 @order.DispatchedAt.Value.ToString("MMM dd, HH:mm")</div>
}
@if (order.ExpectedDeliveryDate.HasValue)
{
<div>📅 Expected @order.ExpectedDeliveryDate.Value.ToString("MMM dd")</div>
}
@if (order.OnHoldAt.HasValue)
{
<div class="text-warning">⏸️ On Hold: @order.OnHoldReason</div>
}
</div>
</div>
<div class="col-auto">
<!-- Mobile Action Buttons -->
<div class="d-grid gap-1" style="min-width: 100px;">
<a href="@Url.Action("Details", new { id = order.Id })" class="btn btn-outline-primary btn-sm">
<i class="fas fa-eye"></i> View
</a>
@if (order.Status == LittleShop.Enums.OrderStatus.PaymentReceived)
{
<form method="post" action="@Url.Action("AcceptOrder", new { id = order.Id })">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-success btn-sm w-100">
<i class="fas fa-check"></i> Accept
</button>
</form>
}
@if (order.Status == LittleShop.Enums.OrderStatus.Accepted)
{
<form method="post" action="@Url.Action("StartPacking", new { id = order.Id })">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-warning btn-sm w-100">
<i class="fas fa-box"></i> Pack
</button>
</form>
}
@if (order.Status == LittleShop.Enums.OrderStatus.Packing)
{
<button type="button" class="btn btn-info btn-sm w-100" data-bs-toggle="modal" data-bs-target="#dispatchModal-@order.Id">
<i class="fas fa-shipping-fast"></i> Dispatch
</button>
}
@if (order.Status != LittleShop.Enums.OrderStatus.OnHold && order.Status != LittleShop.Enums.OrderStatus.Delivered && order.Status != LittleShop.Enums.OrderStatus.Cancelled)
{
<button type="button" class="btn btn-secondary btn-sm w-100" data-bs-toggle="modal" data-bs-target="#holdModal-@order.Id">
<i class="fas fa-pause"></i> Hold
</button>
}
</div>
</div>
</div>
</div>
</div>
}
</div>
}
else
{
<div class="text-center py-4">
<i class="fas fa-clipboard-list fa-3x text-muted mb-3"></i>
<p class="text-muted">No orders found in this category.</p>
@if (currentTab == "accept")
{
<p class="text-muted">Orders will appear here when payment is received.</p>
}
</div>
}
</div>
</div>
@* Dispatch Modals for Packing orders *@
@foreach (var order in orders.Where(o => o.Status == LittleShop.Enums.OrderStatus.Packing))
{
<div class="modal fade" id="dispatchModal-@order.Id" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="@Url.Action("DispatchOrder", new { id = order.Id })">
@Html.AntiForgeryToken()
<div class="modal-header">
<h5 class="modal-title">Dispatch Order #@order.Id.ToString().Substring(0, 8)</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="trackingNumber-@order.Id" class="form-label">Tracking Number <span class="text-danger">*</span></label>
<input name="trackingNumber" id="trackingNumber-@order.Id" class="form-control" placeholder="e.g., RM123456789GB" required />
<small class="form-text text-muted">Royal Mail or courier tracking number</small>
</div>
<div class="mb-3">
<label for="estimatedDays-@order.Id" class="form-label">Estimated Delivery Days</label>
<input type="number" name="estimatedDays" id="estimatedDays-@order.Id" class="form-control" value="3" min="1" max="30" />
<small class="form-text text-muted">Working days until expected delivery</small>
</div>
<div class="mb-3">
<label for="dispatchNotes-@order.Id" class="form-label">Dispatch Notes</label>
<textarea name="notes" id="dispatchNotes-@order.Id" class="form-control" rows="2" placeholder="Optional courier info or special instructions..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-info">
<i class="fas fa-shipping-fast"></i> Dispatch Order
</button>
</div>
</form>
</div>
</div>
</div>
}
@* Hold Modals for each order *@
@foreach (var order in orders.Where(o => o.Status != LittleShop.Enums.OrderStatus.OnHold && o.Status != LittleShop.Enums.OrderStatus.Delivered && o.Status != LittleShop.Enums.OrderStatus.Cancelled))
{
<div class="modal fade" id="holdModal-@order.Id" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="@Url.Action("PutOnHold", new { id = order.Id })">
@Html.AntiForgeryToken()
<div class="modal-header">
<h5 class="modal-title">Put Order On Hold</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="reason-@order.Id" class="form-label">Reason for Hold</label>
<input name="reason" id="reason-@order.Id" class="form-control" placeholder="e.g., Awaiting stock, Customer query" required />
</div>
<div class="mb-3">
<label for="notes-@order.Id" class="form-label">Additional Notes</label>
<textarea name="notes" id="notes-@order.Id" class="form-control" rows="2" placeholder="Optional additional details..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-warning">Put On Hold</button>
</div>
</form>
</div>
</div>
</div>
}