Initial commit
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
using PowderCoating.Shared.Constants;
|
||||
|
||||
namespace PowderCoating.Web.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight API endpoints consumed by the client-side item wizard.
|
||||
/// Intentionally uses <c>CanManageJobs</c> (not <c>CanManageProducts</c>) so that
|
||||
/// any user who can create quotes or jobs can also save items to the catalog directly
|
||||
/// from the wizard without needing a separate catalog-management permission.
|
||||
/// </summary>
|
||||
[Authorize(Policy = AppConstants.Policies.CanManageJobs)]
|
||||
public class ItemWizardController : Controller
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
private readonly ILogger<ItemWizardController> _logger;
|
||||
|
||||
public ItemWizardController(IUnitOfWork unitOfWork, ITenantContext tenantContext,
|
||||
ILogger<ItemWizardController> logger)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_tenantContext = tenantContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns active catalog categories for the current company with their full hierarchy path
|
||||
/// (e.g. "Automotive > Wheels") so the wizard dropdown can distinguish categories that share
|
||||
/// the same name but sit under different parents. Paths are built in-memory after a single
|
||||
/// query — no extra round-trips needed since ParentCategoryId is a scalar field.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetCatalogCategories()
|
||||
{
|
||||
var companyId = _tenantContext.GetCurrentCompanyId();
|
||||
if (companyId == null) return Json(Array.Empty<object>());
|
||||
|
||||
var cats = await _unitOfWork.CatalogCategories.FindAsync(
|
||||
c => c.CompanyId == companyId.Value && !c.IsDeleted);
|
||||
|
||||
var lookup = cats.ToDictionary(c => c.Id);
|
||||
|
||||
string BuildPath(PowderCoating.Core.Entities.CatalogCategory cat)
|
||||
{
|
||||
var parts = new System.Collections.Generic.List<string> { cat.Name };
|
||||
var current = cat;
|
||||
while (current.ParentCategoryId.HasValue &&
|
||||
lookup.TryGetValue(current.ParentCategoryId.Value, out var parent))
|
||||
{
|
||||
parts.Insert(0, parent.Name);
|
||||
current = parent;
|
||||
}
|
||||
return string.Join(" > ", parts);
|
||||
}
|
||||
|
||||
return Json(cats
|
||||
.Select(c => new { c.Id, Name = BuildPath(c) })
|
||||
.OrderBy(c => c.Name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new catalog item from the item wizard's Save-to-Catalog step.
|
||||
/// Called via AJAX immediately when the user confirms the save — before the quote
|
||||
/// or job form is submitted — so the catalog item is saved even if the user later
|
||||
/// abandons the quote.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> SaveToCatalog([FromBody] SaveToCatalogRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var companyId = _tenantContext.GetCurrentCompanyId();
|
||||
if (companyId == null)
|
||||
return Json(new { ok = false, error = "No company context." });
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
return Json(new { ok = false, error = "Item name is required." });
|
||||
|
||||
if (request.CategoryId <= 0)
|
||||
return Json(new { ok = false, error = "Please select a category." });
|
||||
|
||||
var category = await _unitOfWork.CatalogCategories.GetByIdAsync(request.CategoryId);
|
||||
if (category == null || category.CompanyId != companyId.Value)
|
||||
return Json(new { ok = false, error = "Invalid category." });
|
||||
|
||||
var item = new CatalogItem
|
||||
{
|
||||
CompanyId = companyId.Value,
|
||||
Name = request.Name.Trim(),
|
||||
Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(),
|
||||
CategoryId = request.CategoryId,
|
||||
DefaultPrice = request.DefaultPrice,
|
||||
ApproximateArea = request.ApproximateArea > 0 ? request.ApproximateArea : null,
|
||||
DefaultEstimatedMinutes = request.DefaultEstimatedMinutes > 0 ? request.DefaultEstimatedMinutes : null,
|
||||
DefaultRequiresSandblasting = request.DefaultRequiresSandblasting,
|
||||
DefaultRequiresMasking = request.DefaultRequiresMasking,
|
||||
IsActive = true,
|
||||
DisplayOrder = 0
|
||||
};
|
||||
|
||||
await _unitOfWork.CatalogItems.AddAsync(item);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
_logger.LogInformation("Catalog item '{Name}' (id {Id}) created from item wizard by company {CompanyId}",
|
||||
item.Name, item.Id, companyId.Value);
|
||||
|
||||
return Json(new { ok = true, id = item.Id, name = item.Name });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error saving catalog item from wizard");
|
||||
return Json(new { ok = false, error = "An error occurred saving the item. Please try again." });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SaveToCatalogRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public int CategoryId { get; set; }
|
||||
public decimal DefaultPrice { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public decimal ApproximateArea { get; set; }
|
||||
public int DefaultEstimatedMinutes { get; set; }
|
||||
public bool DefaultRequiresSandblasting { get; set; }
|
||||
public bool DefaultRequiresMasking { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user