Product-enhancements-and-validation-fixes

This commit is contained in:
sysadmin
2025-09-01 08:03:00 +01:00
parent c8a55c143b
commit ee4a5c3578
12 changed files with 340 additions and 211 deletions

View File

@@ -20,6 +20,11 @@ public class ProductsController : Controller
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();
return View(products);
}
@@ -34,17 +39,31 @@ public class ProductsController : Controller
[HttpPost]
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}");
// Remove Description validation errors since it's optional
ModelState.Remove("Description");
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();
ViewData["Categories"] = categories.Where(c => c.IsActive);
return View(model);
}
await _productService.CreateProductAsync(model);
var createdProduct = await _productService.CreateProductAsync(model);
return RedirectToAction(nameof(Index));
}
@@ -68,6 +87,7 @@ public class ProductsController : Controller
WeightUnit = product.WeightUnit,
Weight = product.Weight,
Price = product.Price,
StockQuantity = product.StockQuantity,
CategoryId = product.CategoryId,
IsActive = product.IsActive
};

View File

@@ -17,46 +17,86 @@
<div class="card-body">
<form method="post" asp-area="Admin" asp-controller="Products" asp-action="Create">
@Html.AntiForgeryToken()
@if (ViewData.ModelState[""] != null && ViewData.ModelState[""].Errors.Count > 0)
@if (!ViewData.ModelState.IsValid)
{
<div class="alert alert-danger" role="alert">
@foreach (var error in ViewData.ModelState[""].Errors)
{
<div>@error.ErrorMessage</div>
}
<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))
{
<li>@error.ErrorMessage</li>
}
</ul>
</div>
}
<div class="mb-3">
<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 class="mb-3">
<label for="Description" class="form-label">Description</label>
<textarea name="Description" id="Description" class="form-control" rows="4" required></textarea>
<label for="Description" class="form-label">Description <small class="text-muted">(optional)</small></label>
<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 class="row">
<div class="col-md-6">
<div class="col-md-4">
<div class="mb-3">
<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 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">
<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>
@if (categories != null)
{
@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>
@if(ViewData.ModelState["CategoryId"]?.Errors.Count > 0)
{
<div class="invalid-feedback">
@ViewData.ModelState["CategoryId"]?.Errors.FirstOrDefault()?.ErrorMessage
</div>
}
</div>
</div>
</div>
@@ -65,21 +105,33 @@
<div class="col-md-6">
<div class="mb-3">
<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 class="col-md-6">
<div class="mb-3">
<label for="WeightUnit" class="form-label">Unit</label>
<select name="WeightUnit" id="WeightUnit" class="form-select">
<option value="0">Unit</option>
<option value="1">Micrograms</option>
<option value="2">Grams</option>
<option value="3">Ounces</option>
<option value="4">Pounds</option>
<option value="5">Millilitres</option>
<option value="6">Litres</option>
<select name="WeightUnit" id="WeightUnit" class="form-select @(ViewData.ModelState["WeightUnit"]?.Errors.Count > 0 ? "is-invalid" : "")">
<option value="0" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Unit)">Unit</option>
<option value="1" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Micrograms)">Micrograms</option>
<option value="2" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Grams)">Grams</option>
<option value="3" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Ounces)">Ounces</option>
<option value="4" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Pounds)">Pounds</option>
<option value="5" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Millilitres)">Millilitres</option>
<option value="6" selected="@(Model?.WeightUnit == LittleShop.Enums.ProductWeightUnit.Litres)">Litres</option>
</select>
@if(ViewData.ModelState["WeightUnit"]?.Errors.Count > 0)
{
<div class="invalid-feedback">
@ViewData.ModelState["WeightUnit"]?.Errors.FirstOrDefault()?.ErrorMessage
</div>
}
</div>
</div>
</div>
@@ -105,13 +157,99 @@
<div class="card-body">
<ul class="list-unstyled">
<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>Stock:</strong> Current inventory quantity</li>
<li><strong>Weight/Volume:</strong> Used for shipping calculations</li>
<li><strong>Category:</strong> Product organization</li>
<li><strong>Photos:</strong> Can be added after creating the product</li>
</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>
@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>
}

View File

@@ -40,13 +40,19 @@
</div>
<div class="row">
<div class="col-md-6">
<div class="col-md-4">
<div class="mb-3">
<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 />
</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">
<label for="CategoryId" class="form-label">Category</label>
<select name="CategoryId" id="CategoryId" class="form-select" required>

View File

@@ -27,6 +27,7 @@
<th>Name</th>
<th>Category</th>
<th>Price</th>
<th>Stock</th>
<th>Weight</th>
<th>Status</th>
<th>Actions</th>
@@ -58,6 +59,16 @@
<td>
<strong>£@product.Price</strong>
</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>
@product.Weight @product.WeightUnit.ToString().ToLower()
</td>