132 lines
5.5 KiB
C#
132 lines
5.5 KiB
C#
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; }
|
|
}
|