ed35362c7a
- Formula Library ratings: thumbs up/down per company per formula; toggle on/off; sorts by net score; own formulas not rateable; FormulaLibraryRating entity + migration AddFormulaLibraryRatings - Job Profitability report: actual labor cost (logged hours x StandardLaborRate) vs powder cost vs billed price per job; gross margin % color-coded; time-tracked-only filter; totals footer - Quote Revision History: track Total price changes on every save; log Sent/Resent events with recipient email; replace flat table with grouped timeline UI (icons per event type, total-change badge on header) - Setup Wizard: cap CompletedCount at TotalSteps so old 10-step data no longer shows 10/5 - Formula Library card: fix badge overflow on long titles; add Rate: label to make voting buttons discoverable - Help docs and AI knowledge base updated for all three features Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
143 lines
5.2 KiB
C#
143 lines
5.2 KiB
C#
using AutoMapper;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using PowderCoating.Application.Interfaces;
|
|
using PowderCoating.Core.Interfaces;
|
|
using PowderCoating.Shared.Constants;
|
|
|
|
namespace PowderCoating.Web.Controllers;
|
|
|
|
/// <summary>
|
|
/// Community formula library — browse published formulas from all companies and import
|
|
/// them into the current company's local template list.
|
|
/// </summary>
|
|
[Authorize(Policy = AppConstants.Policies.CanViewData)]
|
|
public class FormulaLibraryController : Controller
|
|
{
|
|
private readonly IFormulaLibraryService _libraryService;
|
|
private readonly ITenantContext _tenantContext;
|
|
private readonly IMapper _mapper;
|
|
private readonly IAzureBlobStorageService _blobStorage;
|
|
|
|
public FormulaLibraryController(
|
|
IFormulaLibraryService libraryService,
|
|
ITenantContext tenantContext,
|
|
IMapper mapper,
|
|
IAzureBlobStorageService blobStorage)
|
|
{
|
|
_libraryService = libraryService;
|
|
_tenantContext = tenantContext;
|
|
_mapper = mapper;
|
|
_blobStorage = blobStorage;
|
|
}
|
|
|
|
/// <summary>Browse the community library with optional search and filter params.</summary>
|
|
// GET: /FormulaLibrary
|
|
public async Task<IActionResult> Index(
|
|
string? search = null,
|
|
string? outputMode = null,
|
|
string? industryHint = null)
|
|
{
|
|
var companyId = _tenantContext.GetCurrentCompanyId();
|
|
if (companyId == null) return RedirectToAction("Index", "Home");
|
|
|
|
var items = await _libraryService.BrowseAsync(companyId.Value, search, outputMode, industryHint);
|
|
|
|
ViewBag.Search = search;
|
|
ViewBag.OutputMode = outputMode;
|
|
ViewBag.IndustryHint = industryHint;
|
|
ViewBag.TotalCount = items.Count();
|
|
|
|
return View(items);
|
|
}
|
|
|
|
/// <summary>Returns full detail JSON for the import preview modal.</summary>
|
|
// GET: /FormulaLibrary/Detail/5
|
|
[HttpGet]
|
|
public async Task<IActionResult> Detail(int id)
|
|
{
|
|
var companyId = _tenantContext.GetCurrentCompanyId();
|
|
if (companyId == null) return Json(new { error = "No company context." });
|
|
|
|
var detail = await _libraryService.GetDetailAsync(id, companyId.Value);
|
|
if (detail == null) return NotFound();
|
|
|
|
return Json(detail);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serves a formula diagram image by blob storage path. Used for library cards where the
|
|
/// diagram belongs to another company's template blob container.
|
|
/// </summary>
|
|
// GET: /FormulaLibrary/Diagram?path=...
|
|
[HttpGet]
|
|
public async Task<IActionResult> Diagram(string path)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path)) return NotFound();
|
|
|
|
// Sanitize: path must not escape the blob container
|
|
if (path.Contains("..") || path.StartsWith("/") || path.StartsWith("\\"))
|
|
return BadRequest();
|
|
|
|
var (ok, bytes, contentType, _) = await _blobStorage.DownloadAsync("formulatemplate-diagrams", path);
|
|
if (!ok || bytes == null || bytes.Length == 0) return NotFound();
|
|
return File(bytes, contentType ?? "image/jpeg");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records or toggles a thumbs-up/down vote for the current company.
|
|
/// Returns updated counts so the UI can update without a page reload.
|
|
/// Companies cannot rate their own formulas; own-formula cards have no rating buttons.
|
|
/// </summary>
|
|
// POST: /FormulaLibrary/Rate
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Rate([FromBody] RateFormulaRequest request)
|
|
{
|
|
var companyId = _tenantContext.GetCurrentCompanyId();
|
|
if (companyId == null) return Json(new { success = false, message = "No company context." });
|
|
|
|
try
|
|
{
|
|
var (up, down, myVote) = await _libraryService.RateAsync(
|
|
request.LibraryItemId, companyId.Value, request.IsPositive);
|
|
return Json(new { success = true, thumbsUp = up, thumbsDown = down, myVote });
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
return Json(new { success = false, message = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>Imports a library entry as a new local template for the current company.</summary>
|
|
// POST: /FormulaLibrary/Import
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
[Authorize(Policy = AppConstants.Policies.CompanyAdminOnly)]
|
|
public async Task<IActionResult> Import(int libraryItemId)
|
|
{
|
|
var companyId = _tenantContext.GetCurrentCompanyId();
|
|
if (companyId == null)
|
|
return Json(new { success = false, message = "No company context." });
|
|
|
|
try
|
|
{
|
|
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "";
|
|
var templateId = await _libraryService.ImportAsync(libraryItemId, companyId.Value, userId);
|
|
return Json(new { success = true, templateId });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Json(new { success = false, message = ex.Message });
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Body for the Rate endpoint.</summary>
|
|
public class RateFormulaRequest
|
|
{
|
|
public int LibraryItemId { get; set; }
|
|
/// <summary>True = thumbs up, false = thumbs down.</summary>
|
|
public bool IsPositive { get; set; }
|
|
}
|