From 5adf1b90d5c250c8abf5704137c9710e9f10b7ca Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Thu, 2 Oct 2025 14:35:52 +0100 Subject: [PATCH] Refactor: Streamline product management UI and enhance PWA behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **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 --- .../Admin/Controllers/ProductsController.cs | 243 ------------------ .../Areas/Admin/Views/Products/Edit.cshtml | 16 ++ .../Areas/Admin/Views/Products/Index.cshtml | 38 +-- .../Areas/Admin/Views/Shared/_Layout.cshtml | 27 +- LittleShop/wwwroot/js/notifications.js | 39 ++- LittleShop/wwwroot/js/pwa.js | 19 +- 6 files changed, 96 insertions(+), 286 deletions(-) diff --git a/LittleShop/Areas/Admin/Controllers/ProductsController.cs b/LittleShop/Areas/Admin/Controllers/ProductsController.cs index da3a910..e2702c1 100644 --- a/LittleShop/Areas/Admin/Controllers/ProductsController.cs +++ b/LittleShop/Areas/Admin/Controllers/ProductsController.cs @@ -34,12 +34,6 @@ public class ProductsController : Controller return View(products); } - public IActionResult Blazor(Guid? id) - { - ViewData["ProductId"] = id; - return View(); - } - public async Task Create() { var categories = await _categoryService.GetAllCategoriesAsync(); @@ -193,243 +187,6 @@ public class ProductsController : Controller return RedirectToAction(nameof(Index)); } - // Product Variations - public async Task 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 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 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 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 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 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 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 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 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 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 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 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() { diff --git a/LittleShop/Areas/Admin/Views/Products/Edit.cshtml b/LittleShop/Areas/Admin/Views/Products/Edit.cshtml index 0e9494a..4868444 100644 --- a/LittleShop/Areas/Admin/Views/Products/Edit.cshtml +++ b/LittleShop/Areas/Admin/Views/Products/Edit.cshtml @@ -442,6 +442,22 @@ + +
+
+
Danger Zone
+
+
+

Once you delete a product, there is no going back. Please be certain.

+
+ @Html.AntiForgeryToken() + +
+
+
diff --git a/LittleShop/Areas/Admin/Views/Products/Index.cshtml b/LittleShop/Areas/Admin/Views/Products/Index.cshtml index c6e81e1..c102b91 100644 --- a/LittleShop/Areas/Admin/Views/Products/Index.cshtml +++ b/LittleShop/Areas/Admin/Views/Products/Index.cshtml @@ -9,20 +9,9 @@

Products

@@ -109,24 +98,9 @@ } -
- - - - - - - - - -
- @Html.AntiForgeryToken() - -
-
+ + Details + } diff --git a/LittleShop/Areas/Admin/Views/Shared/_Layout.cshtml b/LittleShop/Areas/Admin/Views/Shared/_Layout.cshtml index 3c73141..e9cf8c4 100644 --- a/LittleShop/Areas/Admin/Views/Shared/_Layout.cshtml +++ b/LittleShop/Areas/Admin/Views/Shared/_Layout.cshtml @@ -46,9 +46,6 @@ TeleShop Admin - - + `; document.body.appendChild(promptDiv); @@ -84,6 +110,7 @@ class AdminNotificationManager { try { await this.enableNotifications(); promptDiv.remove(); + sessionStorage.setItem('notificationPromptDismissed', 'true'); } catch (error) { console.error('Failed to enable notifications:', error); this.showNotificationError('Failed to enable notifications. Please try again.'); @@ -92,8 +119,14 @@ class AdminNotificationManager { document.getElementById('remind-later').addEventListener('click', () => { promptDiv.remove(); - // Set reminder for 1 hour - setTimeout(() => this.showAdminNotificationPrompt(), 60 * 60 * 1000); + // Mark as dismissed for this session only + sessionStorage.setItem('notificationPromptDismissed', 'true'); + }); + + document.getElementById('close-notification-prompt').addEventListener('click', () => { + promptDiv.remove(); + // Mark as dismissed for this session only + sessionStorage.setItem('notificationPromptDismissed', 'true'); }); } diff --git a/LittleShop/wwwroot/js/pwa.js b/LittleShop/wwwroot/js/pwa.js index b6371e1..e9c4404 100644 --- a/LittleShop/wwwroot/js/pwa.js +++ b/LittleShop/wwwroot/js/pwa.js @@ -220,10 +220,20 @@ class PWAManager { } showManualInstallButton() { + // Don't show if PWA is not supported or already installed if (this.isInstalled() || document.getElementById('pwa-install-btn')) { return; } + // Only show if browser supports PWA (has beforeinstallprompt event or is iOS) + const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; + const supportsPWA = 'serviceWorker' in navigator && ('BeforeInstallPromptEvent' in window || isIOS); + + if (!supportsPWA) { + console.log('PWA: Browser does not support PWA installation'); + return; + } + const installBtn = document.createElement('button'); installBtn.id = 'pwa-install-btn'; installBtn.className = 'btn btn-primary btn-sm'; @@ -269,12 +279,17 @@ class PWAManager { const isChrome = navigator.userAgent.includes('Chrome'); const isEdge = navigator.userAgent.includes('Edge'); const isFirefox = navigator.userAgent.includes('Firefox'); + const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; let instructions = 'To install this app:\n\n'; - if (isChrome || isEdge) { + if (isIOS) { + instructions += '1. Tap the Share button (□↑) in Safari\n'; + instructions += '2. Scroll down and tap "Add to Home Screen"\n'; + instructions += '3. Tap "Add" to install'; + } else if (isChrome || isEdge) { instructions += '1. Look for the install icon (⬇️) in the address bar\n'; - instructions += '2. Or click the browser menu (⋮) → "Install LittleShop Admin"\n'; + instructions += '2. Or click the browser menu (⋮) → "Install TeleShop Admin"\n'; instructions += '3. Or check if there\'s an "Install app" option in the browser menu'; } else if (isFirefox) { instructions += '1. Firefox doesn\'t support PWA installation yet\n';