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>
This commit is contained in:
@@ -21,6 +21,7 @@ public class ProductService : IProductService
|
||||
return await _context.Products
|
||||
.Include(p => p.Category)
|
||||
.Include(p => p.Photos)
|
||||
.Include(p => p.Variations.Where(v => v.IsActive))
|
||||
.Where(p => p.IsActive)
|
||||
.Select(p => new ProductDto
|
||||
{
|
||||
@@ -43,6 +44,20 @@ public class ProductService : IProductService
|
||||
FilePath = ph.FilePath,
|
||||
AltText = ph.AltText,
|
||||
SortOrder = ph.SortOrder
|
||||
}).ToList(),
|
||||
Variations = p.Variations.OrderBy(v => v.SortOrder).Select(v => new ProductVariationDto
|
||||
{
|
||||
Id = v.Id,
|
||||
ProductId = v.ProductId,
|
||||
Name = v.Name,
|
||||
Description = v.Description,
|
||||
Quantity = v.Quantity,
|
||||
Price = v.Price,
|
||||
PricePerUnit = v.PricePerUnit,
|
||||
SortOrder = v.SortOrder,
|
||||
IsActive = v.IsActive,
|
||||
CreatedAt = v.CreatedAt,
|
||||
UpdatedAt = v.UpdatedAt
|
||||
}).ToList()
|
||||
})
|
||||
.ToListAsync();
|
||||
@@ -53,6 +68,7 @@ public class ProductService : IProductService
|
||||
return await _context.Products
|
||||
.Include(p => p.Category)
|
||||
.Include(p => p.Photos)
|
||||
.Include(p => p.Variations.Where(v => v.IsActive))
|
||||
.Where(p => p.IsActive && p.CategoryId == categoryId)
|
||||
.Select(p => new ProductDto
|
||||
{
|
||||
@@ -75,6 +91,20 @@ public class ProductService : IProductService
|
||||
FilePath = ph.FilePath,
|
||||
AltText = ph.AltText,
|
||||
SortOrder = ph.SortOrder
|
||||
}).ToList(),
|
||||
Variations = p.Variations.OrderBy(v => v.SortOrder).Select(v => new ProductVariationDto
|
||||
{
|
||||
Id = v.Id,
|
||||
ProductId = v.ProductId,
|
||||
Name = v.Name,
|
||||
Description = v.Description,
|
||||
Quantity = v.Quantity,
|
||||
Price = v.Price,
|
||||
PricePerUnit = v.PricePerUnit,
|
||||
SortOrder = v.SortOrder,
|
||||
IsActive = v.IsActive,
|
||||
CreatedAt = v.CreatedAt,
|
||||
UpdatedAt = v.UpdatedAt
|
||||
}).ToList()
|
||||
})
|
||||
.ToListAsync();
|
||||
@@ -85,6 +115,7 @@ public class ProductService : IProductService
|
||||
var product = await _context.Products
|
||||
.Include(p => p.Category)
|
||||
.Include(p => p.Photos)
|
||||
.Include(p => p.Variations.Where(v => v.IsActive))
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
|
||||
if (product == null) return null;
|
||||
@@ -110,6 +141,20 @@ public class ProductService : IProductService
|
||||
FilePath = ph.FilePath,
|
||||
AltText = ph.AltText,
|
||||
SortOrder = ph.SortOrder
|
||||
}).ToList(),
|
||||
Variations = product.Variations.OrderBy(v => v.SortOrder).Select(v => new ProductVariationDto
|
||||
{
|
||||
Id = v.Id,
|
||||
ProductId = v.ProductId,
|
||||
Name = v.Name,
|
||||
Description = v.Description,
|
||||
Quantity = v.Quantity,
|
||||
Price = v.Price,
|
||||
PricePerUnit = v.PricePerUnit,
|
||||
SortOrder = v.SortOrder,
|
||||
IsActive = v.IsActive,
|
||||
CreatedAt = v.CreatedAt,
|
||||
UpdatedAt = v.UpdatedAt
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
@@ -149,7 +194,8 @@ public class ProductService : IProductService
|
||||
CreatedAt = product.CreatedAt,
|
||||
UpdatedAt = product.UpdatedAt,
|
||||
IsActive = product.IsActive,
|
||||
Photos = new List<ProductPhotoDto>()
|
||||
Photos = new List<ProductPhotoDto>(),
|
||||
Variations = new List<ProductVariationDto>()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -293,6 +339,7 @@ public class ProductService : IProductService
|
||||
var query = _context.Products
|
||||
.Include(p => p.Category)
|
||||
.Include(p => p.Photos)
|
||||
.Include(p => p.Variations.Where(v => v.IsActive))
|
||||
.Where(p => p.IsActive);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
@@ -327,4 +374,149 @@ public class ProductService : IProductService
|
||||
}).ToList()
|
||||
}).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<ProductVariationDto> CreateProductVariationAsync(CreateProductVariationDto createVariationDto)
|
||||
{
|
||||
var product = await _context.Products.FindAsync(createVariationDto.ProductId);
|
||||
if (product == null)
|
||||
throw new ArgumentException("Product not found");
|
||||
|
||||
// Check if variation with this quantity already exists
|
||||
var existingVariation = await _context.ProductVariations
|
||||
.FirstOrDefaultAsync(v => v.ProductId == createVariationDto.ProductId &&
|
||||
v.Quantity == createVariationDto.Quantity &&
|
||||
v.IsActive);
|
||||
|
||||
if (existingVariation != null)
|
||||
throw new ArgumentException($"A variation with quantity {createVariationDto.Quantity} already exists for this product");
|
||||
|
||||
var pricePerUnit = createVariationDto.Price / createVariationDto.Quantity;
|
||||
|
||||
var variation = new ProductVariation
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ProductId = createVariationDto.ProductId,
|
||||
Name = createVariationDto.Name,
|
||||
Description = createVariationDto.Description,
|
||||
Quantity = createVariationDto.Quantity,
|
||||
Price = createVariationDto.Price,
|
||||
PricePerUnit = pricePerUnit,
|
||||
SortOrder = createVariationDto.SortOrder,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.ProductVariations.Add(variation);
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (Microsoft.EntityFrameworkCore.DbUpdateException ex) when (ex.InnerException?.Message.Contains("UNIQUE constraint failed") == true)
|
||||
{
|
||||
throw new ArgumentException($"A variation with quantity {createVariationDto.Quantity} already exists for this product");
|
||||
}
|
||||
|
||||
return new ProductVariationDto
|
||||
{
|
||||
Id = variation.Id,
|
||||
ProductId = variation.ProductId,
|
||||
Name = variation.Name,
|
||||
Description = variation.Description,
|
||||
Quantity = variation.Quantity,
|
||||
Price = variation.Price,
|
||||
PricePerUnit = variation.PricePerUnit,
|
||||
SortOrder = variation.SortOrder,
|
||||
IsActive = variation.IsActive,
|
||||
CreatedAt = variation.CreatedAt,
|
||||
UpdatedAt = variation.UpdatedAt
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateProductVariationAsync(Guid id, UpdateProductVariationDto updateVariationDto)
|
||||
{
|
||||
var variation = await _context.ProductVariations.FindAsync(id);
|
||||
if (variation == null) return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(updateVariationDto.Name))
|
||||
variation.Name = updateVariationDto.Name;
|
||||
|
||||
if (!string.IsNullOrEmpty(updateVariationDto.Description))
|
||||
variation.Description = updateVariationDto.Description;
|
||||
|
||||
if (updateVariationDto.Quantity.HasValue)
|
||||
variation.Quantity = updateVariationDto.Quantity.Value;
|
||||
|
||||
if (updateVariationDto.Price.HasValue)
|
||||
variation.Price = updateVariationDto.Price.Value;
|
||||
|
||||
if (updateVariationDto.Quantity.HasValue || updateVariationDto.Price.HasValue)
|
||||
variation.PricePerUnit = variation.Price / variation.Quantity;
|
||||
|
||||
if (updateVariationDto.SortOrder.HasValue)
|
||||
variation.SortOrder = updateVariationDto.SortOrder.Value;
|
||||
|
||||
if (updateVariationDto.IsActive.HasValue)
|
||||
variation.IsActive = updateVariationDto.IsActive.Value;
|
||||
|
||||
variation.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteProductVariationAsync(Guid id)
|
||||
{
|
||||
var variation = await _context.ProductVariations.FindAsync(id);
|
||||
if (variation == null) return false;
|
||||
|
||||
variation.IsActive = false;
|
||||
variation.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ProductVariationDto>> GetProductVariationsAsync(Guid productId)
|
||||
{
|
||||
return await _context.ProductVariations
|
||||
.Where(v => v.ProductId == productId && v.IsActive)
|
||||
.OrderBy(v => v.SortOrder)
|
||||
.Select(v => new ProductVariationDto
|
||||
{
|
||||
Id = v.Id,
|
||||
ProductId = v.ProductId,
|
||||
Name = v.Name,
|
||||
Description = v.Description,
|
||||
Quantity = v.Quantity,
|
||||
Price = v.Price,
|
||||
PricePerUnit = v.PricePerUnit,
|
||||
SortOrder = v.SortOrder,
|
||||
IsActive = v.IsActive,
|
||||
CreatedAt = v.CreatedAt,
|
||||
UpdatedAt = v.UpdatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<ProductVariationDto?> GetProductVariationByIdAsync(Guid id)
|
||||
{
|
||||
var variation = await _context.ProductVariations.FindAsync(id);
|
||||
if (variation == null) return null;
|
||||
|
||||
return new ProductVariationDto
|
||||
{
|
||||
Id = variation.Id,
|
||||
ProductId = variation.ProductId,
|
||||
Name = variation.Name,
|
||||
Description = variation.Description,
|
||||
Quantity = variation.Quantity,
|
||||
Price = variation.Price,
|
||||
PricePerUnit = variation.PricePerUnit,
|
||||
SortOrder = variation.SortOrder,
|
||||
IsActive = variation.IsActive,
|
||||
CreatedAt = variation.CreatedAt,
|
||||
UpdatedAt = variation.UpdatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user