00bf8a4cd0
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>
138 lines
7.0 KiB
C#
138 lines
7.0 KiB
C#
using AutoMapper;
|
||
using PowderCoating.Application.DTOs.Catalog;
|
||
using PowderCoating.Core.Entities;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
|
||
namespace PowderCoating.Application.Mappings
|
||
{
|
||
/// <summary>
|
||
/// AutoMapper profile for product catalog entities.
|
||
/// </summary>
|
||
public class CatalogProfile : Profile
|
||
{
|
||
public CatalogProfile()
|
||
{
|
||
// ==================== Category Mappings ====================
|
||
|
||
// Category -> CategoryDto
|
||
CreateMap<CatalogCategory, CategoryDto>()
|
||
.ForMember(dest => dest.ParentCategoryName,
|
||
opt => opt.MapFrom(src => src.ParentCategory != null ? src.ParentCategory.Name : null))
|
||
.ForMember(dest => dest.ItemCount,
|
||
opt => opt.MapFrom(src => src.Items.Count(i => !i.IsDeleted)))
|
||
.ForMember(dest => dest.SubCategoryCount,
|
||
opt => opt.MapFrom(src => src.SubCategories.Count(c => !c.IsDeleted)));
|
||
|
||
// CreateCategoryDto -> CatalogCategory
|
||
CreateMap<CreateCategoryDto, CatalogCategory>()
|
||
.ForMember(dest => dest.Id, opt => opt.Ignore())
|
||
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
|
||
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
|
||
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
|
||
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
|
||
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore())
|
||
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
|
||
.ForMember(dest => dest.ParentCategory, opt => opt.Ignore())
|
||
.ForMember(dest => dest.SubCategories, opt => opt.Ignore())
|
||
.ForMember(dest => dest.Items, opt => opt.Ignore());
|
||
|
||
// CatalogCategory -> UpdateCategoryDto (reverse mapping for Edit GET)
|
||
CreateMap<CatalogCategory, UpdateCategoryDto>();
|
||
|
||
// UpdateCategoryDto -> CatalogCategory
|
||
CreateMap<UpdateCategoryDto, CatalogCategory>()
|
||
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
|
||
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
|
||
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
|
||
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
|
||
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore())
|
||
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
|
||
.ForMember(dest => dest.ParentCategory, opt => opt.Ignore())
|
||
.ForMember(dest => dest.SubCategories, opt => opt.Ignore())
|
||
.ForMember(dest => dest.Items, opt => opt.Ignore());
|
||
|
||
// CatalogCategory -> CategoryTreeDto (recursive)
|
||
CreateMap<CatalogCategory, CategoryTreeDto>()
|
||
.ForMember(dest => dest.ItemCount,
|
||
opt => opt.MapFrom(src => src.Items.Count(i => !i.IsDeleted)))
|
||
.ForMember(dest => dest.HasItems,
|
||
opt => opt.MapFrom(src => src.Items.Any(i => !i.IsDeleted)))
|
||
.ForMember(dest => dest.Children,
|
||
opt => opt.MapFrom(src => src.SubCategories.Where(c => !c.IsDeleted).OrderBy(c => c.DisplayOrder)));
|
||
|
||
// ==================== Item Mappings ====================
|
||
|
||
// CatalogItem -> CatalogItemDto
|
||
CreateMap<CatalogItem, CatalogItemDto>()
|
||
.ForMember(dest => dest.CategoryName,
|
||
opt => opt.MapFrom(src => src.Category.Name))
|
||
.ForMember(dest => dest.FullCategoryPath,
|
||
opt => opt.MapFrom(src => GetCategoryPath(src.Category)))
|
||
.ForMember(dest => dest.RevenueAccountName,
|
||
opt => opt.MapFrom(src => src.RevenueAccount != null
|
||
? $"{src.RevenueAccount.AccountNumber} – {src.RevenueAccount.Name}" : null))
|
||
.ForMember(dest => dest.CogsAccountName,
|
||
opt => opt.MapFrom(src => src.CogsAccount != null
|
||
? $"{src.CogsAccount.AccountNumber} – {src.CogsAccount.Name}" : null));
|
||
|
||
// CatalogItem -> CatalogItemListDto
|
||
CreateMap<CatalogItem, CatalogItemListDto>()
|
||
.ForMember(dest => dest.CategoryName,
|
||
opt => opt.MapFrom(src => src.Category.Name));
|
||
|
||
// CreateCatalogItemDto -> CatalogItem
|
||
CreateMap<CreateCatalogItemDto, CatalogItem>()
|
||
.ForMember(dest => dest.Id, opt => opt.Ignore())
|
||
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
|
||
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
|
||
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
|
||
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
|
||
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore())
|
||
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
|
||
.ForMember(dest => dest.Category, opt => opt.Ignore())
|
||
.ForMember(dest => dest.RevenueAccount, opt => opt.Ignore())
|
||
.ForMember(dest => dest.CogsAccount, opt => opt.Ignore())
|
||
// Image paths are set by CatalogImageService after the entity is saved, not from the DTO.
|
||
.ForMember(dest => dest.ImagePath, opt => opt.Ignore())
|
||
.ForMember(dest => dest.ThumbnailPath, opt => opt.Ignore());
|
||
|
||
// UpdateCatalogItemDto -> CatalogItem
|
||
CreateMap<UpdateCatalogItemDto, CatalogItem>()
|
||
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
|
||
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
|
||
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
|
||
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
|
||
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore())
|
||
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
|
||
.ForMember(dest => dest.Category, opt => opt.Ignore())
|
||
.ForMember(dest => dest.RevenueAccount, opt => opt.Ignore())
|
||
.ForMember(dest => dest.CogsAccount, opt => opt.Ignore())
|
||
.ForMember(dest => dest.ImagePath, opt => opt.Ignore())
|
||
.ForMember(dest => dest.ThumbnailPath, opt => opt.Ignore());
|
||
|
||
// CatalogItem -> UpdateCatalogItemDto (reverse mapping for Edit)
|
||
CreateMap<CatalogItem, UpdateCatalogItemDto>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Builds the full category path by walking up the hierarchy.
|
||
/// Example: "Wheels > 18 inch"
|
||
/// </summary>
|
||
private string GetCategoryPath(CatalogCategory category)
|
||
{
|
||
var path = new List<string>();
|
||
var current = category;
|
||
|
||
// Walk up the hierarchy
|
||
while (current != null)
|
||
{
|
||
path.Insert(0, current.Name);
|
||
current = current.ParentCategory;
|
||
}
|
||
|
||
return string.Join(" > ", path);
|
||
}
|
||
}
|
||
}
|