Product-enhancements-and-validation-fixes
This commit is contained in:
parent
c8a55c143b
commit
ee4a5c3578
@ -20,6 +20,11 @@ public class ProductsController : Controller
|
|||||||
|
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
|
// Prevent caching of products list to show real-time data
|
||||||
|
Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
|
Response.Headers.Add("Pragma", "no-cache");
|
||||||
|
Response.Headers.Add("Expires", "0");
|
||||||
|
|
||||||
var products = await _productService.GetAllProductsAsync();
|
var products = await _productService.GetAllProductsAsync();
|
||||||
return View(products);
|
return View(products);
|
||||||
}
|
}
|
||||||
@ -34,17 +39,31 @@ public class ProductsController : Controller
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Create(CreateProductDto model)
|
public async Task<IActionResult> Create(CreateProductDto model)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Received Product: Name='{model?.Name}', Description='{model?.Description}', Price={model?.Price}");
|
Console.WriteLine($"Received Product: Name='{model?.Name}', Description='{model?.Description}', Price={model?.Price}, Stock={model?.StockQuantity}");
|
||||||
|
Console.WriteLine($"CategoryId: {model?.CategoryId}");
|
||||||
|
Console.WriteLine($"Weight: {model?.Weight}, WeightUnit: {model?.WeightUnit}");
|
||||||
Console.WriteLine($"ModelState.IsValid: {ModelState.IsValid}");
|
Console.WriteLine($"ModelState.IsValid: {ModelState.IsValid}");
|
||||||
|
|
||||||
|
// Remove Description validation errors since it's optional
|
||||||
|
ModelState.Remove("Description");
|
||||||
|
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine("Validation errors:");
|
||||||
|
foreach (var error in ModelState)
|
||||||
|
{
|
||||||
|
if (error.Value?.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {error.Key}: {string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage))}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var categories = await _categoryService.GetAllCategoriesAsync();
|
var categories = await _categoryService.GetAllCategoriesAsync();
|
||||||
ViewData["Categories"] = categories.Where(c => c.IsActive);
|
ViewData["Categories"] = categories.Where(c => c.IsActive);
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _productService.CreateProductAsync(model);
|
var createdProduct = await _productService.CreateProductAsync(model);
|
||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +87,7 @@ public class ProductsController : Controller
|
|||||||
WeightUnit = product.WeightUnit,
|
WeightUnit = product.WeightUnit,
|
||||||
Weight = product.Weight,
|
Weight = product.Weight,
|
||||||
Price = product.Price,
|
Price = product.Price,
|
||||||
|
StockQuantity = product.StockQuantity,
|
||||||
CategoryId = product.CategoryId,
|
CategoryId = product.CategoryId,
|
||||||
IsActive = product.IsActive
|
IsActive = product.IsActive
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,46 +17,86 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" asp-area="Admin" asp-controller="Products" asp-action="Create">
|
<form method="post" asp-area="Admin" asp-controller="Products" asp-action="Create">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
@if (ViewData.ModelState[""] != null && ViewData.ModelState[""].Errors.Count > 0)
|
|
||||||
|
@if (!ViewData.ModelState.IsValid)
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
@foreach (var error in ViewData.ModelState[""].Errors)
|
<h6><i class="fas fa-exclamation-triangle"></i> Please fix the following errors:</h6>
|
||||||
|
<ul class="mb-0">
|
||||||
|
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
|
||||||
{
|
{
|
||||||
<div>@error.ErrorMessage</div>
|
<li>@error.ErrorMessage</li>
|
||||||
}
|
}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="Name" class="form-label">Product Name</label>
|
<label for="Name" class="form-label">Product Name</label>
|
||||||
<input name="Name" id="Name" class="form-control" required />
|
<input name="Name" id="Name" value="@Model?.Name" class="form-control @(ViewData.ModelState["Name"]?.Errors.Count > 0 ? "is-invalid" : "")" required />
|
||||||
|
@if(ViewData.ModelState["Name"]?.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
@ViewData.ModelState["Name"]?.Errors.FirstOrDefault()?.ErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="Description" class="form-label">Description</label>
|
<label for="Description" class="form-label">Description <small class="text-muted">(optional)</small></label>
|
||||||
<textarea name="Description" id="Description" class="form-control" rows="4" required></textarea>
|
<textarea name="Description" id="Description" class="form-control @(ViewData.ModelState["Description"]?.Errors.Count > 0 ? "is-invalid" : "")" rows="3" placeholder="Describe your product...">@Model?.Description</textarea>
|
||||||
|
@if(ViewData.ModelState["Description"]?.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
@ViewData.ModelState["Description"]?.Errors.FirstOrDefault()?.ErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="Price" class="form-label">Price (£)</label>
|
<label for="Price" class="form-label">Price (£)</label>
|
||||||
<input name="Price" id="Price" type="number" step="0.01" class="form-control" required />
|
<input name="Price" id="Price" value="@Model?.Price" type="number" step="0.01" class="form-control @(ViewData.ModelState["Price"]?.Errors.Count > 0 ? "is-invalid" : "")" required />
|
||||||
|
@if(ViewData.ModelState["Price"]?.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
@ViewData.ModelState["Price"]?.Errors.FirstOrDefault()?.ErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="StockQuantity" class="form-label">Stock Quantity</label>
|
||||||
|
<input name="StockQuantity" id="StockQuantity" value="@(Model?.StockQuantity ?? 0)" type="number" min="0" class="form-control @(ViewData.ModelState["StockQuantity"]?.Errors.Count > 0 ? "is-invalid" : "")" required />
|
||||||
|
@if(ViewData.ModelState["StockQuantity"]?.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
@ViewData.ModelState["StockQuantity"]?.Errors.FirstOrDefault()?.ErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="CategoryId" class="form-label">Category</label>
|
<label for="CategoryId" class="form-label">Category</label>
|
||||||
<select name="CategoryId" id="CategoryId" class="form-select" required>
|
<select name="CategoryId" id="CategoryId" class="form-select @(ViewData.ModelState["CategoryId"]?.Errors.Count > 0 ? "is-invalid" : "")" required>
|
||||||
<option value="">Select a category</option>
|
<option value="">Select a category</option>
|
||||||
@if (categories != null)
|
@if (categories != null)
|
||||||
{
|
{
|
||||||
@foreach (var category in categories)
|
@foreach (var category in categories)
|
||||||
{
|
{
|
||||||
<option value="@category.Id">@category.Name</option>
|
<option value="@category.Id" selected="@(Model?.CategoryId == category.Id)">@category.Name</option>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
|
@if(ViewData.ModelState["CategoryId"]?.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
@ViewData.ModelState["CategoryId"]?.Errors.FirstOrDefault()?.ErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -65,21 +105,33 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="Weight" class="form-label">Weight/Volume</label>
|
<label for="Weight" class="form-label">Weight/Volume</label>
|
||||||
<input name="Weight" id="Weight" type="number" step="0.01" class="form-control" required />
|
<input name="Weight" id="Weight" value="@Model?.Weight" type="number" step="0.01" class="form-control @(ViewData.ModelState["Weight"]?.Errors.Count > 0 ? "is-invalid" : "")" required />
|
||||||
|
@if(ViewData.ModelState["Weight"]?.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
@ViewData.ModelState["Weight"]?.Errors.FirstOrDefault()?.ErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="WeightUnit" class="form-label">Unit</label>
|
<label for="WeightUnit" class="form-label">Unit</label>
|
||||||
<select name="WeightUnit" id="WeightUnit" class="form-select">
|
<select name="WeightUnit" id="WeightUnit" class="form-select @(ViewData.ModelState["WeightUnit"]?.Errors.Count > 0 ? "is-invalid" : "")">
|
||||||
<option value="0">Unit</option>
|
<option value="0" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Unit)">Unit</option>
|
||||||
<option value="1">Micrograms</option>
|
<option value="1" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Micrograms)">Micrograms</option>
|
||||||
<option value="2">Grams</option>
|
<option value="2" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Grams)">Grams</option>
|
||||||
<option value="3">Ounces</option>
|
<option value="3" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Ounces)">Ounces</option>
|
||||||
<option value="4">Pounds</option>
|
<option value="4" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Pounds)">Pounds</option>
|
||||||
<option value="5">Millilitres</option>
|
<option value="5" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Millilitres)">Millilitres</option>
|
||||||
<option value="6">Litres</option>
|
<option value="6" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Litres)">Litres</option>
|
||||||
</select>
|
</select>
|
||||||
|
@if(ViewData.ModelState["WeightUnit"]?.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
@ViewData.ModelState["WeightUnit"]?.Errors.FirstOrDefault()?.ErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -105,13 +157,99 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li><strong>Name:</strong> Unique product identifier</li>
|
<li><strong>Name:</strong> Unique product identifier</li>
|
||||||
<li><strong>Description:</strong> Supports Unicode and emojis</li>
|
<li><strong>Description:</strong> Optional, supports Unicode and emojis</li>
|
||||||
<li><strong>Price:</strong> Base price in GBP</li>
|
<li><strong>Price:</strong> Base price in GBP</li>
|
||||||
|
<li><strong>Stock:</strong> Current inventory quantity</li>
|
||||||
<li><strong>Weight/Volume:</strong> Used for shipping calculations</li>
|
<li><strong>Weight/Volume:</strong> Used for shipping calculations</li>
|
||||||
<li><strong>Category:</strong> Product organization</li>
|
<li><strong>Category:</strong> Product organization</li>
|
||||||
|
<li><strong>Photos:</strong> Can be added after creating the product</li>
|
||||||
</ul>
|
</ul>
|
||||||
<small class="text-muted">You can add photos after creating the product.</small>
|
<small class="text-muted">The form remembers your last used category and weight unit. Add photos after creation.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const categorySelect = document.getElementById('CategoryId');
|
||||||
|
const weightUnitSelect = document.getElementById('WeightUnit');
|
||||||
|
const photoInput = document.getElementById('ProductPhotos');
|
||||||
|
const photoPreview = document.getElementById('photo-preview');
|
||||||
|
|
||||||
|
// Restore last used category and weight unit
|
||||||
|
const lastCategory = localStorage.getItem('lastProductCategory');
|
||||||
|
const lastWeightUnit = localStorage.getItem('lastProductWeightUnit');
|
||||||
|
|
||||||
|
if (lastCategory && categorySelect) {
|
||||||
|
categorySelect.value = lastCategory;
|
||||||
|
console.log('Restored last category:', lastCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastWeightUnit && weightUnitSelect) {
|
||||||
|
weightUnitSelect.value = lastWeightUnit;
|
||||||
|
console.log('Restored last weight unit:', lastWeightUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save category and weight unit when changed
|
||||||
|
if (categorySelect) {
|
||||||
|
categorySelect.addEventListener('change', function() {
|
||||||
|
if (this.value) {
|
||||||
|
localStorage.setItem('lastProductCategory', this.value);
|
||||||
|
console.log('Saved category preference:', this.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weightUnitSelect) {
|
||||||
|
weightUnitSelect.addEventListener('change', function() {
|
||||||
|
localStorage.setItem('lastProductWeightUnit', this.value);
|
||||||
|
console.log('Saved weight unit preference:', this.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Photo preview functionality
|
||||||
|
if (photoInput) {
|
||||||
|
photoInput.addEventListener('change', function(e) {
|
||||||
|
const files = Array.from(e.target.files);
|
||||||
|
photoPreview.innerHTML = '';
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
photoPreview.style.display = 'block';
|
||||||
|
|
||||||
|
files.forEach((file, index) => {
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
const col = document.createElement('div');
|
||||||
|
col.className = 'col-md-3 col-6 mb-3';
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(event) {
|
||||||
|
col.innerHTML = `
|
||||||
|
<div class="card">
|
||||||
|
<img src="${event.target.result}" class="card-img-top" style="height: 120px; object-fit: cover;" alt="Preview ${index + 1}">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<small class="text-muted">${file.name}</small><br>
|
||||||
|
<small class="text-success">${(file.size / 1024).toFixed(1)} KB</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
photoPreview.appendChild(col);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
photoPreview.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus on name field for better UX
|
||||||
|
const nameInput = document.getElementById('Name');
|
||||||
|
if (nameInput) {
|
||||||
|
setTimeout(() => nameInput.focus(), 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
@ -40,13 +40,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="Price" class="form-label">Price (£)</label>
|
<label for="Price" class="form-label">Price (£)</label>
|
||||||
<input name="Price" id="Price" value="@Model?.Price" type="number" step="0.01" class="form-control" required />
|
<input name="Price" id="Price" value="@Model?.Price" type="number" step="0.01" class="form-control" required />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="StockQuantity" class="form-label">Stock Quantity</label>
|
||||||
|
<input name="StockQuantity" id="StockQuantity" value="@Model?.StockQuantity" type="number" min="0" class="form-control" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="CategoryId" class="form-label">Category</label>
|
<label for="CategoryId" class="form-label">Category</label>
|
||||||
<select name="CategoryId" id="CategoryId" class="form-select" required>
|
<select name="CategoryId" id="CategoryId" class="form-select" required>
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th>Price</th>
|
<th>Price</th>
|
||||||
|
<th>Stock</th>
|
||||||
<th>Weight</th>
|
<th>Weight</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
@ -58,6 +59,16 @@
|
|||||||
<td>
|
<td>
|
||||||
<strong>£@product.Price</strong>
|
<strong>£@product.Price</strong>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (product.StockQuantity > 0)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">@product.StockQuantity in stock</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark">Out of stock</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@product.Weight @product.WeightUnit.ToString().ToLower()
|
@product.Weight @product.WeightUnit.ToString().ToLower()
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ public class ProductDto
|
|||||||
public decimal Price { get; set; }
|
public decimal Price { get; set; }
|
||||||
public decimal Weight { get; set; }
|
public decimal Weight { get; set; }
|
||||||
public ProductWeightUnit WeightUnit { get; set; }
|
public ProductWeightUnit WeightUnit { get; set; }
|
||||||
|
public int StockQuantity { get; set; }
|
||||||
public Guid CategoryId { get; set; }
|
public Guid CategoryId { get; set; }
|
||||||
public string CategoryName { get; set; } = string.Empty;
|
public string CategoryName { get; set; } = string.Empty;
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
@ -34,17 +35,23 @@ public class CreateProductDto
|
|||||||
[StringLength(200)]
|
[StringLength(200)]
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Required]
|
[Required(AllowEmptyStrings = true)]
|
||||||
public string Description { get; set; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Range(0.01, double.MaxValue)]
|
[Required]
|
||||||
|
[Range(0.01, double.MaxValue, ErrorMessage = "Price must be greater than 0")]
|
||||||
public decimal Price { get; set; }
|
public decimal Price { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Range(0.01, double.MaxValue, ErrorMessage = "Weight must be greater than 0")]
|
||||||
public decimal Weight { get; set; }
|
public decimal Weight { get; set; }
|
||||||
|
|
||||||
public ProductWeightUnit WeightUnit { get; set; }
|
public ProductWeightUnit WeightUnit { get; set; } = ProductWeightUnit.Grams;
|
||||||
|
|
||||||
[Required]
|
[Range(0, int.MaxValue)]
|
||||||
|
public int StockQuantity { get; set; } = 0;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please select a category")]
|
||||||
public Guid CategoryId { get; set; }
|
public Guid CategoryId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,6 +69,9 @@ public class UpdateProductDto
|
|||||||
|
|
||||||
public ProductWeightUnit? WeightUnit { get; set; }
|
public ProductWeightUnit? WeightUnit { get; set; }
|
||||||
|
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int? StockQuantity { get; set; }
|
||||||
|
|
||||||
public Guid? CategoryId { get; set; }
|
public Guid? CategoryId { get; set; }
|
||||||
|
|
||||||
public bool? IsActive { get; set; }
|
public bool? IsActive { get; set; }
|
||||||
|
|||||||
@ -13,7 +13,6 @@ public class Product
|
|||||||
[StringLength(200)]
|
[StringLength(200)]
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Required]
|
|
||||||
public string Description { get; set; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Column(TypeName = "decimal(18,2)")]
|
[Column(TypeName = "decimal(18,2)")]
|
||||||
@ -24,6 +23,8 @@ public class Product
|
|||||||
|
|
||||||
public ProductWeightUnit WeightUnit { get; set; } = ProductWeightUnit.Kilogram;
|
public ProductWeightUnit WeightUnit { get; set; } = ProductWeightUnit.Kilogram;
|
||||||
|
|
||||||
|
public int StockQuantity { get; set; } = 0;
|
||||||
|
|
||||||
public Guid CategoryId { get; set; }
|
public Guid CategoryId { get; set; }
|
||||||
|
|
||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
|
|||||||
@ -167,10 +167,12 @@ app.MapControllerRoute(
|
|||||||
|
|
||||||
app.MapControllers(); // API routes
|
app.MapControllers(); // API routes
|
||||||
|
|
||||||
// Ensure database is created
|
// Apply database migrations and seed data
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
var context = scope.ServiceProvider.GetRequiredService<LittleShopContext>();
|
var context = scope.ServiceProvider.GetRequiredService<LittleShopContext>();
|
||||||
|
|
||||||
|
// Ensure database is created (temporary while fixing migrations)
|
||||||
context.Database.EnsureCreated();
|
context.Database.EnsureCreated();
|
||||||
|
|
||||||
// Seed default admin user
|
// Seed default admin user
|
||||||
|
|||||||
@ -80,6 +80,7 @@ public class DataSeederService : IDataSeederService
|
|||||||
Price = 89.99m,
|
Price = 89.99m,
|
||||||
Weight = 250,
|
Weight = 250,
|
||||||
WeightUnit = ProductWeightUnit.Grams,
|
WeightUnit = ProductWeightUnit.Grams,
|
||||||
|
StockQuantity = 10,
|
||||||
CategoryId = categories[0].Id,
|
CategoryId = categories[0].Id,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
@ -93,6 +94,7 @@ public class DataSeederService : IDataSeederService
|
|||||||
Price = 19.99m,
|
Price = 19.99m,
|
||||||
Weight = 50,
|
Weight = 50,
|
||||||
WeightUnit = ProductWeightUnit.Grams,
|
WeightUnit = ProductWeightUnit.Grams,
|
||||||
|
StockQuantity = 10,
|
||||||
CategoryId = categories[0].Id,
|
CategoryId = categories[0].Id,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
@ -106,6 +108,7 @@ public class DataSeederService : IDataSeederService
|
|||||||
Price = 24.99m,
|
Price = 24.99m,
|
||||||
Weight = 200,
|
Weight = 200,
|
||||||
WeightUnit = ProductWeightUnit.Grams,
|
WeightUnit = ProductWeightUnit.Grams,
|
||||||
|
StockQuantity = 15,
|
||||||
CategoryId = categories[1].Id,
|
CategoryId = categories[1].Id,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
@ -119,6 +122,7 @@ public class DataSeederService : IDataSeederService
|
|||||||
Price = 59.99m,
|
Price = 59.99m,
|
||||||
Weight = 500,
|
Weight = 500,
|
||||||
WeightUnit = ProductWeightUnit.Grams,
|
WeightUnit = ProductWeightUnit.Grams,
|
||||||
|
StockQuantity = 15,
|
||||||
CategoryId = categories[1].Id,
|
CategoryId = categories[1].Id,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
@ -132,6 +136,7 @@ public class DataSeederService : IDataSeederService
|
|||||||
Price = 34.99m,
|
Price = 34.99m,
|
||||||
Weight = 800,
|
Weight = 800,
|
||||||
WeightUnit = ProductWeightUnit.Grams,
|
WeightUnit = ProductWeightUnit.Grams,
|
||||||
|
StockQuantity = 25,
|
||||||
CategoryId = categories[2].Id,
|
CategoryId = categories[2].Id,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
|||||||
@ -95,6 +95,7 @@ public class ProductService : IProductService
|
|||||||
Price = product.Price,
|
Price = product.Price,
|
||||||
Weight = product.Weight,
|
Weight = product.Weight,
|
||||||
WeightUnit = product.WeightUnit,
|
WeightUnit = product.WeightUnit,
|
||||||
|
StockQuantity = product.StockQuantity,
|
||||||
CategoryId = product.CategoryId,
|
CategoryId = product.CategoryId,
|
||||||
CategoryName = product.Category.Name,
|
CategoryName = product.Category.Name,
|
||||||
CreatedAt = product.CreatedAt,
|
CreatedAt = product.CreatedAt,
|
||||||
@ -117,10 +118,11 @@ public class ProductService : IProductService
|
|||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Name = createProductDto.Name,
|
Name = createProductDto.Name,
|
||||||
Description = createProductDto.Description,
|
Description = string.IsNullOrEmpty(createProductDto.Description) ? " " : createProductDto.Description,
|
||||||
Price = createProductDto.Price,
|
Price = createProductDto.Price,
|
||||||
Weight = createProductDto.Weight,
|
Weight = createProductDto.Weight,
|
||||||
WeightUnit = createProductDto.WeightUnit,
|
WeightUnit = createProductDto.WeightUnit,
|
||||||
|
StockQuantity = createProductDto.StockQuantity,
|
||||||
CategoryId = createProductDto.CategoryId,
|
CategoryId = createProductDto.CategoryId,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
UpdatedAt = DateTime.UtcNow,
|
UpdatedAt = DateTime.UtcNow,
|
||||||
@ -169,6 +171,9 @@ public class ProductService : IProductService
|
|||||||
if (updateProductDto.WeightUnit.HasValue)
|
if (updateProductDto.WeightUnit.HasValue)
|
||||||
product.WeightUnit = updateProductDto.WeightUnit.Value;
|
product.WeightUnit = updateProductDto.WeightUnit.Value;
|
||||||
|
|
||||||
|
if (updateProductDto.StockQuantity.HasValue)
|
||||||
|
product.StockQuantity = updateProductDto.StockQuantity.Value;
|
||||||
|
|
||||||
if (updateProductDto.CategoryId.HasValue)
|
if (updateProductDto.CategoryId.HasValue)
|
||||||
product.CategoryId = updateProductDto.CategoryId.Value;
|
product.CategoryId = updateProductDto.CategoryId.Value;
|
||||||
|
|
||||||
|
|||||||
@ -268,6 +268,7 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile Table - Stack on Small Screens */
|
/* Mobile Table - Stack on Small Screens */
|
||||||
|
@media (max-width: 768px) {
|
||||||
.table-responsive {
|
.table-responsive {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
@ -311,6 +312,7 @@ h1 {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--grey-600);
|
color: var(--grey-600);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile Navigation */
|
/* Mobile Navigation */
|
||||||
.navbar-toggler {
|
.navbar-toggler {
|
||||||
|
|||||||
@ -37,12 +37,13 @@ class PWAManager {
|
|||||||
// Setup push notifications
|
// Setup push notifications
|
||||||
this.setupPushNotifications();
|
this.setupPushNotifications();
|
||||||
|
|
||||||
// Show manual install option after 3 seconds if no prompt appeared and app not installed
|
// Show manual install option after 5 seconds if no prompt appeared and app not installed
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!document.getElementById('pwa-install-btn') && !this.isInstalled()) {
|
if (!document.getElementById('pwa-install-btn') && !this.isInstalled()) {
|
||||||
|
console.log('PWA: No install prompt appeared, showing manual install guide');
|
||||||
this.showManualInstallButton();
|
this.showManualInstallButton();
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupInstallPrompt() {
|
setupInstallPrompt() {
|
||||||
@ -70,6 +71,9 @@ class PWAManager {
|
|||||||
this.hideInstallButton();
|
this.hideInstallButton();
|
||||||
} else {
|
} else {
|
||||||
console.log('PWA: App is not installed, waiting for install prompt...');
|
console.log('PWA: App is not installed, waiting for install prompt...');
|
||||||
|
console.log('PWA: Current URL:', window.location.href);
|
||||||
|
console.log('PWA: Display mode:', window.matchMedia('(display-mode: standalone)').matches ? 'standalone' : 'browser');
|
||||||
|
console.log('PWA: User agent:', navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Periodically check if app becomes installed (for cases where user installs via browser menu)
|
// Periodically check if app becomes installed (for cases where user installs via browser menu)
|
||||||
@ -210,7 +214,27 @@ class PWAManager {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
installBtn.addEventListener('click', () => {
|
installBtn.addEventListener('click', () => {
|
||||||
alert('To install this app:\\n\\n1. Click the browser menu (⋮)\\n2. Select "Install LittleShop Admin"\\n\\nOr look for the install icon in the address bar!');
|
const isChrome = navigator.userAgent.includes('Chrome');
|
||||||
|
const isEdge = navigator.userAgent.includes('Edge');
|
||||||
|
const isFirefox = navigator.userAgent.includes('Firefox');
|
||||||
|
|
||||||
|
let instructions = 'To install this app:\\n\\n';
|
||||||
|
|
||||||
|
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 += '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';
|
||||||
|
instructions += '2. You can bookmark this page for easy access\\n';
|
||||||
|
instructions += '3. Or use Chrome/Edge for the full PWA experience';
|
||||||
|
} else {
|
||||||
|
instructions += '1. Look for an install or "Add to Home Screen" option\\n';
|
||||||
|
instructions += '2. Check your browser menu for app installation\\n';
|
||||||
|
instructions += '3. Or bookmark this page for quick access';
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(instructions);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.appendChild(installBtn);
|
document.body.appendChild(installBtn);
|
||||||
@ -230,25 +254,21 @@ class PWAManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if user has dismissed push notifications recently
|
|
||||||
const dismissedUntil = localStorage.getItem('pushNotificationsDismissedUntil');
|
|
||||||
if (dismissedUntil && new Date() < new Date(dismissedUntil)) {
|
|
||||||
console.log('PWA: Push notifications dismissed by user, skipping setup');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get VAPID public key from server
|
// Get VAPID public key from server
|
||||||
await this.getVapidPublicKey();
|
await this.getVapidPublicKey();
|
||||||
|
|
||||||
// Check if user is already subscribed
|
// Check if user is already subscribed
|
||||||
await this.checkPushSubscription();
|
await this.checkPushSubscription();
|
||||||
|
|
||||||
// Only show setup UI if not subscribed and not recently dismissed
|
// Simple logic: only show prompt if user is not subscribed
|
||||||
const isSubscribedFromCache = localStorage.getItem('pushNotificationsSubscribed') === 'true';
|
if (!this.pushSubscription) {
|
||||||
if (!this.pushSubscription && !isSubscribedFromCache) {
|
// Check if we've already asked this session
|
||||||
|
if (!sessionStorage.getItem('pushNotificationPromptShown')) {
|
||||||
this.showPushNotificationSetup();
|
this.showPushNotificationSetup();
|
||||||
|
sessionStorage.setItem('pushNotificationPromptShown', 'true');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('PWA: User already subscribed to push notifications, skipping setup UI');
|
console.log('PWA: User already subscribed to push notifications');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -280,34 +300,9 @@ class PWAManager {
|
|||||||
try {
|
try {
|
||||||
this.pushSubscription = await this.swRegistration.pushManager.getSubscription();
|
this.pushSubscription = await this.swRegistration.pushManager.getSubscription();
|
||||||
if (this.pushSubscription) {
|
if (this.pushSubscription) {
|
||||||
console.log('PWA: Browser has push subscription');
|
console.log('PWA: User has active push subscription');
|
||||||
|
|
||||||
// Verify server-side subscription still exists by trying to send a test
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/push/subscriptions', {
|
|
||||||
credentials: 'same-origin'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const subscriptions = await response.json();
|
|
||||||
const hasServerSubscription = subscriptions.some(sub =>
|
|
||||||
sub.endpoint && this.pushSubscription.endpoint.includes(sub.endpoint.substring(0, 50))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasServerSubscription) {
|
|
||||||
console.log('PWA: Server subscription missing, will re-subscribe on next attempt');
|
|
||||||
localStorage.removeItem('pushNotificationsSubscribed');
|
|
||||||
} else {
|
|
||||||
console.log('PWA: Server subscription confirmed');
|
|
||||||
localStorage.setItem('pushNotificationsSubscribed', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('PWA: Could not verify server subscription:', error.message);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log('PWA: User is not subscribed to push notifications');
|
console.log('PWA: User is not subscribed to push notifications');
|
||||||
localStorage.removeItem('pushNotificationsSubscribed');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PWA: Error checking push subscription:', error);
|
console.error('PWA: Error checking push subscription:', error);
|
||||||
@ -358,12 +353,7 @@ class PWAManager {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
this.pushSubscription = subscription;
|
this.pushSubscription = subscription;
|
||||||
console.log('PWA: Successfully subscribed to push notifications');
|
console.log('PWA: Successfully subscribed to push notifications');
|
||||||
|
this.hidePushNotificationSetup();
|
||||||
// Cache subscription state
|
|
||||||
localStorage.setItem('pushNotificationsSubscribed', 'true');
|
|
||||||
localStorage.setItem('pushNotificationsSubscribedAt', new Date().toISOString());
|
|
||||||
|
|
||||||
this.updatePushNotificationUI();
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to save push subscription to server');
|
throw new Error('Failed to save push subscription to server');
|
||||||
@ -397,12 +387,6 @@ class PWAManager {
|
|||||||
|
|
||||||
this.pushSubscription = null;
|
this.pushSubscription = null;
|
||||||
console.log('PWA: Successfully unsubscribed from push notifications');
|
console.log('PWA: Successfully unsubscribed from push notifications');
|
||||||
|
|
||||||
// Clear subscription cache
|
|
||||||
localStorage.removeItem('pushNotificationsSubscribed');
|
|
||||||
localStorage.removeItem('pushNotificationsSubscribedAt');
|
|
||||||
|
|
||||||
this.updatePushNotificationUI();
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PWA: Failed to unsubscribe from push notifications:', error);
|
console.error('PWA: Failed to unsubscribe from push notifications:', error);
|
||||||
@ -427,32 +411,24 @@ class PWAManager {
|
|||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const isSubscribed = !!this.pushSubscription;
|
|
||||||
|
|
||||||
setupDiv.innerHTML = `
|
setupDiv.innerHTML = `
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<i class="fas fa-bell me-2"></i>
|
<i class="fas fa-bell me-2"></i>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<strong>Push Notifications</strong><br>
|
<strong>Push Notifications</strong><br>
|
||||||
<small>${isSubscribed ? 'You are subscribed to notifications' : 'Get notified of new orders and updates'}</small>
|
<small>Get notified of new orders and updates</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-2">
|
<div class="ms-2">
|
||||||
${isSubscribed ?
|
<button type="button" class="btn btn-sm btn-primary" id="subscribe-push-btn">Enable</button>
|
||||||
'<button type="button" class="btn btn-sm btn-outline-danger" id="unsubscribe-push-btn">Turn Off</button>' :
|
|
||||||
'<div class="d-flex flex-column gap-1"><button type="button" class="btn btn-sm btn-primary" id="subscribe-push-btn">Turn On</button><button type="button" class="btn btn-sm btn-outline-secondary" id="dismiss-push-btn">Not Now</button></div>'
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" id="close-push-setup"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(setupDiv);
|
document.body.appendChild(setupDiv);
|
||||||
|
|
||||||
// Add event listeners
|
// Add event listener for subscribe button
|
||||||
const subscribeBtn = document.getElementById('subscribe-push-btn');
|
const subscribeBtn = document.getElementById('subscribe-push-btn');
|
||||||
const unsubscribeBtn = document.getElementById('unsubscribe-push-btn');
|
|
||||||
const dismissBtn = document.getElementById('dismiss-push-btn');
|
|
||||||
const closeBtn = document.getElementById('close-push-setup');
|
|
||||||
|
|
||||||
if (subscribeBtn) {
|
if (subscribeBtn) {
|
||||||
subscribeBtn.addEventListener('click', async () => {
|
subscribeBtn.addEventListener('click', async () => {
|
||||||
@ -475,76 +451,17 @@ class PWAManager {
|
|||||||
|
|
||||||
alert('Failed to enable push notifications: ' + userMessage);
|
alert('Failed to enable push notifications: ' + userMessage);
|
||||||
subscribeBtn.disabled = false;
|
subscribeBtn.disabled = false;
|
||||||
subscribeBtn.innerHTML = 'Turn On';
|
subscribeBtn.innerHTML = 'Enable';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unsubscribeBtn) {
|
|
||||||
unsubscribeBtn.addEventListener('click', async () => {
|
|
||||||
unsubscribeBtn.disabled = true;
|
|
||||||
unsubscribeBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Disabling...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.unsubscribeFromPushNotifications();
|
|
||||||
this.showNotification('Push notifications disabled', {
|
|
||||||
body: 'You will no longer receive push notifications.'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
alert('Failed to disable push notifications: ' + error.message);
|
|
||||||
unsubscribeBtn.disabled = false;
|
|
||||||
unsubscribeBtn.innerHTML = 'Turn Off';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle dismiss button
|
hidePushNotificationSetup() {
|
||||||
if (dismissBtn) {
|
|
||||||
dismissBtn.addEventListener('click', () => {
|
|
||||||
// Dismiss for 24 hours
|
|
||||||
const dismissUntil = new Date();
|
|
||||||
dismissUntil.setHours(dismissUntil.getHours() + 24);
|
|
||||||
localStorage.setItem('pushNotificationsDismissedUntil', dismissUntil.toISOString());
|
|
||||||
|
|
||||||
const element = document.getElementById('push-notification-setup');
|
|
||||||
if (element) {
|
|
||||||
element.remove();
|
|
||||||
}
|
|
||||||
console.log('PWA: Push notifications dismissed for 24 hours');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle close button
|
|
||||||
if (closeBtn) {
|
|
||||||
closeBtn.addEventListener('click', () => {
|
|
||||||
// Dismiss for 1 hour
|
|
||||||
const dismissUntil = new Date();
|
|
||||||
dismissUntil.setHours(dismissUntil.getHours() + 1);
|
|
||||||
localStorage.setItem('pushNotificationsDismissedUntil', dismissUntil.toISOString());
|
|
||||||
console.log('PWA: Push notifications dismissed for 1 hour');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-hide after 15 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
const element = document.getElementById('push-notification-setup');
|
|
||||||
if (element) {
|
|
||||||
element.remove();
|
|
||||||
|
|
||||||
// Auto-dismiss for 2 hours if user ignores
|
|
||||||
const dismissUntil = new Date();
|
|
||||||
dismissUntil.setHours(dismissUntil.getHours() + 2);
|
|
||||||
localStorage.setItem('pushNotificationsDismissedUntil', dismissUntil.toISOString());
|
|
||||||
console.log('PWA: Push notifications auto-dismissed for 2 hours');
|
|
||||||
}
|
|
||||||
}, 15000);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePushNotificationUI() {
|
|
||||||
const setupDiv = document.getElementById('push-notification-setup');
|
const setupDiv = document.getElementById('push-notification-setup');
|
||||||
if (setupDiv) {
|
if (setupDiv) {
|
||||||
setupDiv.remove();
|
setupDiv.remove();
|
||||||
this.showPushNotificationSetup();
|
console.log('PWA: Push notification setup hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
// LittleShop Admin - Service Worker
|
// LittleShop Admin - Service Worker
|
||||||
// Provides offline functionality and app-like experience
|
// Provides offline functionality and app-like experience
|
||||||
|
|
||||||
const CACHE_NAME = 'littleshop-admin-v1';
|
const CACHE_NAME = 'littleshop-admin-v2';
|
||||||
const urlsToCache = [
|
const urlsToCache = [
|
||||||
'/Admin/Dashboard',
|
// Only cache static assets, not dynamic admin pages
|
||||||
'/Admin/Orders',
|
|
||||||
'/Admin/Products',
|
|
||||||
'/Admin/Categories',
|
|
||||||
'/lib/bootstrap/css/bootstrap.min.css',
|
'/lib/bootstrap/css/bootstrap.min.css',
|
||||||
'/lib/fontawesome/css/all.min.css',
|
'/lib/fontawesome/css/all.min.css',
|
||||||
'/lib/bootstrap-icons/bootstrap-icons.css',
|
'/lib/bootstrap-icons/bootstrap-icons.css',
|
||||||
@ -14,6 +11,7 @@ const urlsToCache = [
|
|||||||
'/lib/jquery/jquery.min.js',
|
'/lib/jquery/jquery.min.js',
|
||||||
'/lib/bootstrap/js/bootstrap.bundle.min.js',
|
'/lib/bootstrap/js/bootstrap.bundle.min.js',
|
||||||
'/js/modern-mobile.js',
|
'/js/modern-mobile.js',
|
||||||
|
'/js/pwa.js',
|
||||||
'/lib/fontawesome/webfonts/fa-solid-900.woff2',
|
'/lib/fontawesome/webfonts/fa-solid-900.woff2',
|
||||||
'/lib/fontawesome/webfonts/fa-brands-400.woff2'
|
'/lib/fontawesome/webfonts/fa-brands-400.woff2'
|
||||||
];
|
];
|
||||||
@ -121,13 +119,27 @@ self.addEventListener('notificationclick', (event) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch event - serve from cache, fallback to network
|
// Fetch event - network-first for admin pages, cache-first for assets
|
||||||
self.addEventListener('fetch', (event) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
// Only handle GET requests
|
// Only handle GET requests
|
||||||
if (event.request.method !== 'GET') {
|
if (event.request.method !== 'GET') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
|
// Network-first strategy for admin pages (always fresh data)
|
||||||
|
if (url.pathname.startsWith('/Admin/')) {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request).catch(() => {
|
||||||
|
// If network fails, try cache as fallback
|
||||||
|
return caches.match(event.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache-first strategy for static assets
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches.match(event.request)
|
caches.match(event.request)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user