Refactor: Streamline product management UI and enhance PWA behavior
**Product List Improvements:** - Move Import/Export to settings menu for cleaner interface - Replace Edit/Variants/Multi-Buys buttons with single Details action - Remove Blazor UI button from product list - Simplify product row actions for better mobile UX **Product Details Enhancements:** - Add Danger Zone section with Delete button at bottom - Improve visual hierarchy and action placement **Navigation Updates:** - Remove hamburger menu toggle (desktop nav always visible) - Rename Settings to Menu in mobile bottom nav - Update settings drawer header and icon **Code Cleanup:** - Remove unused Blazor, Variations, and Variants endpoints (243 lines) - Consolidate variant/multi-buy management within product details - Clean up ProductsController for better maintainability **PWA & Notifications:** - Add proper PWA support detection (only show if browser supports) - Implement session-based notification prompt tracking - Prevent repeated prompts after dismissal in same session - Respect permanent dismissal preferences - Enhance iOS Safari detection and instructions **Technical Details:** - 6 files changed, 96 insertions(+), 286 deletions(-) - Build successful with 0 errors - All features production-ready 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -34,12 +34,6 @@ public class ProductsController : Controller
|
||||
return View(products);
|
||||
}
|
||||
|
||||
public IActionResult Blazor(Guid? id)
|
||||
{
|
||||
ViewData["ProductId"] = id;
|
||||
return View();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Create()
|
||||
{
|
||||
var categories = await _categoryService.GetAllCategoriesAsync();
|
||||
@@ -193,243 +187,6 @@ public class ProductsController : Controller
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
// Product Variations
|
||||
public async Task<IActionResult> Variations(Guid id)
|
||||
{
|
||||
var product = await _productService.GetProductByIdAsync(id);
|
||||
if (product == null)
|
||||
return NotFound();
|
||||
|
||||
ViewData["Product"] = product;
|
||||
var variations = await _productService.GetProductMultiBuysAsync(id);
|
||||
return View(variations);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> CreateVariation(Guid productId)
|
||||
{
|
||||
var product = await _productService.GetProductByIdAsync(productId);
|
||||
if (product == null)
|
||||
return NotFound();
|
||||
|
||||
// Force no-cache to ensure updated form loads
|
||||
Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
Response.Headers.Add("Pragma", "no-cache");
|
||||
Response.Headers.Add("Expires", "0");
|
||||
|
||||
ViewData["Product"] = product;
|
||||
|
||||
// Get existing quantities to help user avoid duplicates
|
||||
var existingQuantities = await _productService.GetProductMultiBuysAsync(productId);
|
||||
ViewData["ExistingQuantities"] = existingQuantities.Select(v => v.Quantity).ToList();
|
||||
|
||||
return View(new CreateProductMultiBuyDto { ProductId = productId });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> CreateVariation(CreateProductMultiBuyDto model)
|
||||
{
|
||||
// Debug form data
|
||||
Console.WriteLine("=== FORM DEBUG ===");
|
||||
Console.WriteLine($"Received CreateVariation POST: ProductId={model?.ProductId}, Name='{model?.Name}', Quantity={model?.Quantity}, Price={model?.Price}");
|
||||
Console.WriteLine($"ModelState.IsValid: {ModelState.IsValid}");
|
||||
Console.WriteLine("Raw form data:");
|
||||
foreach (var key in Request.Form.Keys)
|
||||
{
|
||||
Console.WriteLine($" {key} = '{Request.Form[key]}'");
|
||||
}
|
||||
Console.WriteLine("================");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
foreach (var key in ModelState.Keys)
|
||||
{
|
||||
var errors = ModelState[key]?.Errors;
|
||||
if (errors?.Any() == true)
|
||||
{
|
||||
Console.WriteLine($"ModelState Error - {key}: {string.Join(", ", errors.Select(e => e.ErrorMessage))}");
|
||||
}
|
||||
}
|
||||
|
||||
var product = await _productService.GetProductByIdAsync(model.ProductId);
|
||||
ViewData["Product"] = product;
|
||||
|
||||
// Re-populate existing quantities for error display
|
||||
var existingQuantities = await _productService.GetProductMultiBuysAsync(model.ProductId);
|
||||
ViewData["ExistingQuantities"] = existingQuantities.Select(v => v.Quantity).ToList();
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _productService.CreateProductMultiBuyAsync(model);
|
||||
return RedirectToAction(nameof(Variations), new { id = model.ProductId });
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
// Add the error to the appropriate field if it's a quantity conflict
|
||||
if (ex.Message.Contains("quantity") && ex.Message.Contains("already exists"))
|
||||
{
|
||||
ModelState.AddModelError("Quantity", ex.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ex.Message);
|
||||
}
|
||||
|
||||
var product = await _productService.GetProductByIdAsync(model.ProductId);
|
||||
ViewData["Product"] = product;
|
||||
|
||||
// Re-populate existing quantities for error display
|
||||
var existingQuantities = await _productService.GetProductMultiBuysAsync(model.ProductId);
|
||||
ViewData["ExistingQuantities"] = existingQuantities.Select(v => v.Quantity).ToList();
|
||||
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> EditVariation(Guid id)
|
||||
{
|
||||
var variation = await _productService.GetProductMultiBuyByIdAsync(id);
|
||||
if (variation == null)
|
||||
return NotFound();
|
||||
|
||||
var product = await _productService.GetProductByIdAsync(variation.ProductId);
|
||||
ViewData["Product"] = product;
|
||||
|
||||
var model = new UpdateProductMultiBuyDto
|
||||
{
|
||||
Name = variation.Name,
|
||||
Description = variation.Description,
|
||||
Quantity = variation.Quantity,
|
||||
Price = variation.Price,
|
||||
SortOrder = variation.SortOrder,
|
||||
IsActive = variation.IsActive
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EditVariation(Guid id, UpdateProductMultiBuyDto model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var variation = await _productService.GetProductMultiBuyByIdAsync(id);
|
||||
var product = await _productService.GetProductByIdAsync(variation!.ProductId);
|
||||
ViewData["Product"] = product;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var success = await _productService.UpdateProductMultiBuyAsync(id, model);
|
||||
if (!success)
|
||||
return NotFound();
|
||||
|
||||
var variationToRedirect = await _productService.GetProductMultiBuyByIdAsync(id);
|
||||
return RedirectToAction(nameof(Variations), new { id = variationToRedirect!.ProductId });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> DeleteVariation(Guid id)
|
||||
{
|
||||
var variation = await _productService.GetProductMultiBuyByIdAsync(id);
|
||||
if (variation == null)
|
||||
return NotFound();
|
||||
|
||||
await _productService.DeleteProductMultiBuyAsync(id);
|
||||
return RedirectToAction(nameof(Variations), new { id = variation.ProductId });
|
||||
}
|
||||
|
||||
// Product Variants (Colors/Flavors)
|
||||
public async Task<IActionResult> Variants(Guid id)
|
||||
{
|
||||
var product = await _productService.GetProductByIdAsync(id);
|
||||
if (product == null)
|
||||
return NotFound();
|
||||
|
||||
ViewData["Product"] = product;
|
||||
var variants = await _productService.GetProductVariantsAsync(id);
|
||||
return View("ProductVariants", variants);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> CreateVariant(Guid productId)
|
||||
{
|
||||
var product = await _productService.GetProductByIdAsync(productId);
|
||||
if (product == null)
|
||||
return NotFound();
|
||||
|
||||
ViewData["Product"] = product;
|
||||
return View("CreateVariant", new CreateProductVariantDto { ProductId = productId });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> CreateVariant(CreateProductVariantDto model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var product = await _productService.GetProductByIdAsync(model.ProductId);
|
||||
ViewData["Product"] = product;
|
||||
return View("CreateVariant", model);
|
||||
}
|
||||
|
||||
await _productService.CreateProductVariantAsync(model);
|
||||
return RedirectToAction(nameof(Variants), new { id = model.ProductId });
|
||||
}
|
||||
|
||||
public async Task<IActionResult> EditVariant(Guid id)
|
||||
{
|
||||
var variant = await _productService.GetProductVariantByIdAsync(id);
|
||||
if (variant == null)
|
||||
return NotFound();
|
||||
|
||||
var product = await _productService.GetProductByIdAsync(variant.ProductId);
|
||||
ViewData["Product"] = product;
|
||||
|
||||
var model = new UpdateProductVariantDto
|
||||
{
|
||||
Name = variant.Name,
|
||||
VariantType = variant.VariantType,
|
||||
StockLevel = variant.StockLevel,
|
||||
SortOrder = variant.SortOrder,
|
||||
IsActive = variant.IsActive
|
||||
};
|
||||
|
||||
return View("EditVariant", model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EditVariant(Guid id, UpdateProductVariantDto model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var variant = await _productService.GetProductVariantByIdAsync(id);
|
||||
var product = await _productService.GetProductByIdAsync(variant!.ProductId);
|
||||
ViewData["Product"] = product;
|
||||
return View("EditVariant", model);
|
||||
}
|
||||
|
||||
await _productService.UpdateProductVariantAsync(id, model);
|
||||
var variantToRedirect = await _productService.GetProductVariantByIdAsync(id);
|
||||
return RedirectToAction(nameof(Variants), new { id = variantToRedirect!.ProductId });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> DeleteVariant(Guid id)
|
||||
{
|
||||
var variant = await _productService.GetProductVariantByIdAsync(id);
|
||||
if (variant == null)
|
||||
return NotFound();
|
||||
|
||||
await _productService.DeleteProductVariantAsync(id);
|
||||
return RedirectToAction(nameof(Variants), new { id = variant.ProductId });
|
||||
}
|
||||
|
||||
// Product Import/Export
|
||||
public IActionResult Import()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user