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>
116 lines
4.4 KiB
C#
116 lines
4.4 KiB
C#
namespace PowderCoating.Core.Entities
|
||
{
|
||
/// <summary>
|
||
/// Represents a reusable item in the product catalog that can be quickly added to quotes.
|
||
/// Stores default values for common powder coating jobs.
|
||
/// </summary>
|
||
public class CatalogItem : BaseEntity
|
||
{
|
||
/// <summary>
|
||
/// Gets or sets the item name.
|
||
/// </summary>
|
||
public string Name { get; set; } = string.Empty;
|
||
|
||
/// <summary>
|
||
/// Gets or sets the optional item description.
|
||
/// </summary>
|
||
public string? Description { get; set; }
|
||
|
||
/// <summary>
|
||
/// Gets or sets the optional SKU (Stock Keeping Unit) or item code.
|
||
/// </summary>
|
||
public string? SKU { get; set; }
|
||
|
||
/// <summary>
|
||
/// Gets or sets the category ID this item belongs to.
|
||
/// </summary>
|
||
public int CategoryId { get; set; }
|
||
|
||
/// <summary>
|
||
/// Gets or sets the category navigation property.
|
||
/// </summary>
|
||
public virtual CatalogCategory Category { get; set; } = null!;
|
||
|
||
/// <summary>
|
||
/// Gets or sets the default price for this item.
|
||
/// Can be overridden when added to a quote.
|
||
/// </summary>
|
||
public decimal DefaultPrice { get; set; }
|
||
|
||
/// <summary>
|
||
/// Gets or sets whether this item typically requires sandblasting.
|
||
/// </summary>
|
||
public bool DefaultRequiresSandblasting { get; set; }
|
||
|
||
/// <summary>
|
||
/// Gets or sets whether this item typically requires masking/taping.
|
||
/// </summary>
|
||
public bool DefaultRequiresMasking { get; set; }
|
||
|
||
/// <summary>
|
||
/// Gets or sets the estimated processing time in minutes.
|
||
/// </summary>
|
||
public int? DefaultEstimatedMinutes { get; set; }
|
||
|
||
/// <summary>
|
||
/// Gets or sets the approximate surface area in square feet (or square meters if using metric).
|
||
/// </summary>
|
||
public decimal? ApproximateArea { get; set; }
|
||
|
||
/// <summary>
|
||
/// Gets or sets the display order for sorting items within a category.
|
||
/// Lower numbers appear first.
|
||
/// </summary>
|
||
public int DisplayOrder { get; set; }
|
||
|
||
/// <summary>
|
||
/// Gets or sets whether this item is currently active and visible.
|
||
/// </summary>
|
||
public bool IsActive { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// When true, this item is retail merchandise available for direct sale on an invoice
|
||
/// without a job (e.g. branded apparel, cleaning products). Inherited from the category
|
||
/// by default but can be overridden per item.
|
||
/// </summary>
|
||
public bool IsMerchandise { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Optional link to an inventory item so stock is decremented when this item is sold.
|
||
/// </summary>
|
||
public int? InventoryItemId { get; set; }
|
||
public virtual InventoryItem? InventoryItem { get; set; }
|
||
|
||
// ── Financial Account Mapping ──────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Gets or sets the revenue account ID for invoicing this item.
|
||
/// When null, falls back to the default revenue account.
|
||
/// </summary>
|
||
public int? RevenueAccountId { get; set; }
|
||
|
||
/// <summary>
|
||
/// Gets or sets the COGS account ID for recording material cost when this item is used.
|
||
/// When null, falls back to the default COGS account.
|
||
/// </summary>
|
||
public int? CogsAccountId { get; set; }
|
||
|
||
public virtual Account? RevenueAccount { get; set; }
|
||
public virtual Account? CogsAccount { get; set; }
|
||
|
||
// ── Images ────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Blob path of the full-size uploaded image, relative to the catalogimages container.
|
||
/// Null when no image has been uploaded.
|
||
/// </summary>
|
||
public string? ImagePath { get; set; }
|
||
|
||
/// <summary>
|
||
/// Blob path of the 200×200 thumbnail generated on upload.
|
||
/// Null when no image has been uploaded.
|
||
/// </summary>
|
||
public string? ThumbnailPath { get; set; }
|
||
}
|
||
}
|