littleshop/LittleShop/Areas/Admin/Views/Orders/Index.cshtml
SysAdmin 2aadd8ed2c feat: Phase 1 - Critical WCAG 2.1 AA accessibility improvements
Implemented comprehensive accessibility enhancements to meet WCAG 2.1 AA standards:

**Skip Navigation:**
- Added skip-to-content link for keyboard users
- Link appears on focus and jumps directly to main content area

**Screen Reader Support:**
- Created .sr-only and .sr-only-focusable utility classes
- Added aria-hidden="true" to all decorative icons
- Added descriptive aria-label attributes to all icon-only buttons

**Enhanced Focus Indicators:**
- Implemented 3px visible outlines on all interactive elements
- Added :focus-visible for keyboard-only focus indicators
- Special focus styling for primary actions (orange outline)
- Consistent 2px outline-offset for better visibility

**Table Accessibility:**
- Added scope="col" attributes to all table headers
- Properly grouped button actions with role="group" and aria-label

**Button Improvements:**
- All icon-only buttons now have descriptive ARIA labels
- Added responsive text labels (visible on sm+ screens, hidden on mobile)
- Improved button groups with proper ARIA roles

**Files Modified:**
- _Layout.cshtml: Skip link, accessible menu close button
- Categories/Index.cshtml: ARIA labels, table scopes
- Users/Index.cshtml: ARIA labels, table scopes
- Orders/Index.cshtml: Table scopes
- Products/Index.cshtml: Table scopes
- ShippingRates/Index.cshtml: ARIA labels, table scopes
- VariantCollections/Index.cshtml: ARIA labels, table scopes
- modern-admin.css: Accessibility utilities and enhanced focus styles

**WCAG 2.1 AA Criteria Addressed:**
- 2.4.1 Bypass Blocks (Level A)
- 2.4.7 Focus Visible (Level AA)
- 4.1.2 Name, Role, Value (Level A)
- 1.3.1 Info and Relationships (Level A)

🚀 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 17:56:28 +00: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 scope="col">Order ID</th>
<th scope="col">Customer</th>
<th scope="col">Items</th>
<th scope="col">Total</th>
<th scope="col">Status</th>
<th scope="col">Timeline</th>
<th scope="col">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>
}