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:
2025-10-02 14:35:52 +01:00
parent 4992b6b839
commit 5adf1b90d5
6 changed files with 96 additions and 286 deletions

View File

@@ -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()
{