littleshop/LittleShop/Areas/Admin/Views/Orders/Index.cshtml
SilverLabs DevTeam a419bd7a78 Implement product variations, enhanced order workflow, mobile responsiveness, and product import system
## Product Variations System
- Add ProductVariation model with quantity-based pricing (1 for £10, 2 for £19, 3 for £25)
- Complete CRUD operations for product variations
- Enhanced ProductService to include variations in all queries
- Updated OrderItem to support ProductVariationId for variation-based orders
- Graceful error handling for duplicate quantity constraints
- Admin interface with variations management (Create/Edit/Delete)
- API endpoints for programmatic variation management

## Enhanced Order Workflow Management
- Redesigned OrderStatus enum with clear workflow states (Accept → Packing → Dispatched → Delivered)
- Added workflow tracking fields (AcceptedAt, PackingStartedAt, DispatchedAt, ExpectedDeliveryDate)
- User tracking for accountability (AcceptedByUser, PackedByUser, DispatchedByUser)
- Automatic delivery date calculation (dispatch date + working days, skips weekends)
- On Hold workflow for problem resolution with reason tracking
- Tab-based orders interface focused on workflow stages
- One-click workflow actions from list view

## Mobile-Responsive Design
- Responsive orders interface: tables on desktop, cards on mobile
- Touch-friendly buttons and spacing for mobile users
- Horizontal scrolling tabs with condensed labels on mobile
- Color-coded status borders for quick visual recognition
- Smart text switching based on screen size

## Product Import/Export System
- CSV import with product variations support
- Template download with examples
- Export existing products to CSV
- Detailed import results with success/error reporting
- Category name resolution (no need for GUIDs)
- Photo URLs import support

## Enhanced Dashboard
- Product variations count and metrics
- Stock alerts (low stock/out of stock warnings)
- Order workflow breakdown (pending, accepted, dispatched counts)
- Enhanced layout with more detailed information

## Technical Improvements
- Fixed form binding issues across all admin forms
- Removed external CDN dependencies for isolated deployment
- Bot Wizard form with auto-personality assignment
- Proper authentication scheme configuration (Cookie + JWT)
- Enhanced debug logging for troubleshooting

## Self-Contained Deployment
- All external CDN references replaced with local libraries
- Ready for air-gapped/isolated network deployment
- No external internet dependencies

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 01:39:31 +01:00

407 lines
23 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 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 == "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.ProductVariationName))
{
<small class="text-muted">(@item.ProductVariationName)</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.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.ProductVariationName))
{
<span class="text-muted">(@firstItem.ProductVariationName)</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.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>
@* 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>
}