Product-enhancements-and-validation-fixes
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user