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; /// /// Community formula library — browse published formulas from all companies and import /// them into the current company's local template list. /// [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; } /// Browse the community library with optional search and filter params. // GET: /FormulaLibrary public async Task 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); } /// Returns full detail JSON for the import preview modal. // GET: /FormulaLibrary/Detail/5 [HttpGet] public async Task 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); } /// /// Serves a formula diagram image by blob storage path. Used for library cards where the /// diagram belongs to another company's template blob container. /// // GET: /FormulaLibrary/Diagram?path=... [HttpGet] public async Task 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"); } /// /// 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. /// // POST: /FormulaLibrary/Rate [HttpPost] [ValidateAntiForgeryToken] public async Task 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 }); } } /// Imports a library entry as a new local template for the current company. // POST: /FormulaLibrary/Import [HttpPost] [ValidateAntiForgeryToken] [Authorize(Policy = AppConstants.Policies.CompanyAdminOnly)] public async Task 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 }); } } } /// Body for the Rate endpoint. public class RateFormulaRequest { public int LibraryItemId { get; set; } /// True = thumbs up, false = thumbs down. public bool IsPositive { get; set; } }