Add catalog item images with thumbnail preview in wizard
Each catalog item now supports one optional image (jpg/jpeg/png/gif/webp,
max 10 MB). Uploading generates a 200x200 JPEG thumbnail automatically via
SixLabors.ImageSharp. Images are stored in Azure Blob Storage under a new
catalogimages container, keyed by {companyId}/catalog/{itemId}/.
- CatalogItem entity: ImagePath + ThumbnailPath (nullable string fields)
- Migration: AddCatalogItemImages applied
- ICatalogImageService / CatalogImageService: upload, thumbnail generation,
delete; old blobs replaced atomically on re-upload
- CatalogItemsController: Create/Edit accept optional IFormFile image;
Image(id, thumbnail) action serves blobs with [Authorize] so wizard users
can load thumbnails without CanManageProducts policy
- Catalog index (_CategoryNode): 40x40 thumbnail (or placeholder icon)
left of each item name
- Details view: image card in right column with click-to-full-size link
- Create/Edit views: file picker with live preview; Edit shows current
thumbnail with Remove checkbox
- Wizard (item-wizard.js): thumbnails in product list with hover preview
that follows the cursor (showCatalogPreview / moveCatalogPreview);
fixed Bootstrap d-flex !important bug that broke the filter box by
moving flex layout to an inner wrapper div
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-action="Create" method="post">
|
||||
<form asp-action="Create" method="post" enctype="multipart/form-data">
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="alert alert-permanent alert-warning d-flex gap-2 mb-4" role="alert">
|
||||
@@ -159,6 +159,17 @@
|
||||
<div class="form-text">When checked, this item appears in the merchandise picker on invoices and can be sold without a job (e.g. branded apparel, retail cleaning products).</div>
|
||||
</div>
|
||||
|
||||
<!-- Item Image -->
|
||||
<h5 class="border-bottom pb-2 mb-3 mt-4">Item Image <span class="text-muted small fw-normal">(optional)</span></h5>
|
||||
<div class="mb-3">
|
||||
<label for="image" class="form-label fw-semibold">Upload Image</label>
|
||||
<input type="file" class="form-control" id="image" name="image" accept="image/jpeg,image/png,image/gif,image/webp" onchange="previewCatalogImage(this)" />
|
||||
<div class="form-text">Accepted formats: jpg, jpeg, png, gif, webp — max 10 MB. A 200×200 thumbnail is generated automatically.</div>
|
||||
<div id="imagePreview" class="mt-2 d-none">
|
||||
<img id="imagePreviewImg" src="" alt="Preview" style="max-width:200px;max-height:200px;object-fit:contain;border:1px solid #dee2e6;border-radius:6px;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
@@ -349,5 +360,17 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function previewCatalogImage(input) {
|
||||
const preview = document.getElementById('imagePreview');
|
||||
const img = document.getElementById('imagePreviewImg');
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => { img.src = e.target.result; preview.classList.remove('d-none'); };
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
} else {
|
||||
preview.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
@@ -134,8 +134,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Actions -->
|
||||
<!-- Right Column: Image + Actions -->
|
||||
<div class="col-lg-4">
|
||||
@if (!string.IsNullOrEmpty(Model.ImagePath))
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0"><i class="bi bi-image me-1"></i>Item Image</h6>
|
||||
</div>
|
||||
<div class="card-body p-2 text-center">
|
||||
<a href="@Url.Action("Image", "CatalogItems", new { id = Model.Id, thumbnail = false })" target="_blank">
|
||||
<img src="@Url.Action("Image", "CatalogItems", new { id = Model.Id, thumbnail = true })"
|
||||
alt="@Model.Name"
|
||||
style="max-width:100%;border-radius:6px;" />
|
||||
</a>
|
||||
<p class="text-muted small mt-1 mb-0">Click to view full size</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Actions</h5>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-action="Edit" method="post">
|
||||
<form asp-action="Edit" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
@@ -159,6 +159,40 @@
|
||||
<div class="form-text">When checked, this item appears in the merchandise picker on invoices and can be sold without a job.</div>
|
||||
</div>
|
||||
|
||||
<!-- Item Image -->
|
||||
<h5 class="border-bottom pb-2 mb-3 mt-4">Item Image <span class="text-muted small fw-normal">(optional)</span></h5>
|
||||
@if (ViewBag.HasImage == true)
|
||||
{
|
||||
<div class="mb-3 d-flex align-items-start gap-3">
|
||||
<div>
|
||||
<img id="imagePreviewImg"
|
||||
src="@Url.Action("Image", "CatalogItems", new { id = Model.Id, thumbnail = true })"
|
||||
alt="Current image"
|
||||
style="width:100px;height:100px;object-fit:cover;border-radius:6px;border:1px solid #dee2e6;" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-1 fw-semibold">Current Image</p>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="removeImage" id="removeImage" value="true" />
|
||||
<label class="form-check-label text-danger" for="removeImage">Remove current image</label>
|
||||
</div>
|
||||
<label for="image" class="form-label text-muted small">Replace with a new image:</label>
|
||||
<input type="file" class="form-control form-control-sm" id="image" name="image" accept="image/jpeg,image/png,image/gif,image/webp" onchange="previewCatalogImage(this)" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label for="image" class="form-label fw-semibold">Upload Image</label>
|
||||
<input type="file" class="form-control" id="image" name="image" accept="image/jpeg,image/png,image/gif,image/webp" onchange="previewCatalogImage(this)" />
|
||||
<div class="form-text">Accepted formats: jpg, jpeg, png, gif, webp — max 10 MB. A 200×200 thumbnail is generated automatically.</div>
|
||||
<div id="imagePreview" class="mt-2 d-none">
|
||||
<img id="imagePreviewImg" src="" alt="Preview" style="max-width:200px;max-height:200px;object-fit:contain;border:1px solid #dee2e6;border-radius:6px;" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
|
||||
@@ -345,5 +379,20 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function previewCatalogImage(input) {
|
||||
const preview = document.getElementById('imagePreview');
|
||||
const img = document.getElementById('imagePreviewImg');
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
img.src = e.target.result;
|
||||
if (preview) preview.classList.remove('d-none');
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
} else {
|
||||
if (preview) preview.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
@@ -34,9 +34,21 @@
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
<div class="item-row">
|
||||
<div class="item-row-name">
|
||||
<div class="item-row-name d-flex align-items-center gap-2">
|
||||
@if (!string.IsNullOrEmpty(item.ThumbnailPath))
|
||||
{
|
||||
<img src="@Url.Action("Image", "CatalogItems", new { id = item.Id, thumbnail = true })"
|
||||
alt="@item.Name"
|
||||
style="width:40px;height:40px;object-fit:cover;border-radius:4px;flex-shrink:0;" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<span style="width:40px;height:40px;background:#f0f0f0;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||
<i class="bi bi-image catalog-text-muted"></i>
|
||||
</span>
|
||||
}
|
||||
<a asp-action="Details" asp-route-id="@item.Id" class="catalog-item-link">
|
||||
<i class="bi bi-box me-2 catalog-text-muted"></i>@item.Name
|
||||
@item.Name
|
||||
</a>
|
||||
</div>
|
||||
<div class="item-row-meta">
|
||||
|
||||
Reference in New Issue
Block a user