Add Community Formula Library feature
Companies can now share their custom formula templates to a platform-wide community library. Other tenants can browse, preview, and import formulas as independent local copies. Includes attribution (source company name), "Inspired by" lineage for re-contributed formulas, import counts, own-formula badge, cascade diagram nullification, and AI assistant + help docs updates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
namespace PowderCoating.Application.DTOs.Company;
|
||||
|
||||
// ── Browse / card display ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Lean DTO for the community library browse grid card.</summary>
|
||||
public class FormulaLibraryCardDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public string OutputMode { get; set; } = "FixedRate";
|
||||
public string? Tags { get; set; }
|
||||
public string? IndustryHint { get; set; }
|
||||
public string SourceCompanyName { get; set; } = string.Empty;
|
||||
public int ImportCount { get; set; }
|
||||
public DateTime SharedAt { get; set; }
|
||||
public string? DiagramImagePath { get; set; }
|
||||
|
||||
/// <summary>Non-null when this formula was derived from another library entry.</summary>
|
||||
public int? InspiredByFormulaLibraryItemId { get; set; }
|
||||
public string? InspiredByName { get; set; }
|
||||
public string? InspiredByCompanyName { get; set; }
|
||||
|
||||
/// <summary>True when the current company has already imported this entry.</summary>
|
||||
public bool AlreadyImported { get; set; }
|
||||
|
||||
/// <summary>True when this formula was shared by the current browsing company.</summary>
|
||||
public bool IsOwnFormula { get; set; }
|
||||
}
|
||||
|
||||
// ── Full detail (import preview modal) ────────────────────────────────────
|
||||
|
||||
/// <summary>Full DTO used in the import preview modal — shows fields and formula.</summary>
|
||||
public class FormulaLibraryDetailDto : FormulaLibraryCardDto
|
||||
{
|
||||
public string FieldsJson { get; set; } = "[]";
|
||||
public string Formula { get; set; } = string.Empty;
|
||||
public decimal? DefaultRate { get; set; }
|
||||
public string? RateLabel { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public int FieldCount { get; set; }
|
||||
}
|
||||
|
||||
// ── Share from Company Settings ───────────────────────────────────────────
|
||||
|
||||
/// <summary>Submitted when a company admin shares one of their templates to the community library.</summary>
|
||||
public class ShareFormulaRequest
|
||||
{
|
||||
public int CustomItemTemplateId { get; set; }
|
||||
public string? Tags { get; set; }
|
||||
public string? IndustryHint { get; set; }
|
||||
}
|
||||
|
||||
// ── Company Settings list view ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>Status of a template relative to the community library, shown in Company Settings.</summary>
|
||||
public class FormulaLibraryStatusDto
|
||||
{
|
||||
/// <summary>The FormulaLibraryItem Id, if this template has ever been shared.</summary>
|
||||
public int? LibraryItemId { get; set; }
|
||||
public bool IsPublished { get; set; }
|
||||
|
||||
/// <summary>Whether this template is eligible to be shared (original or modified import).</summary>
|
||||
public bool CanShare { get; set; }
|
||||
|
||||
/// <summary>Set when this template was imported; the name of the original library entry.</summary>
|
||||
public string? ImportedFromName { get; set; }
|
||||
public string? ImportedFromCompany { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using PowderCoating.Application.DTOs.Company;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the community formula library: sharing, unsharing, importing, and browsing.
|
||||
/// </summary>
|
||||
public interface IFormulaLibraryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all published library entries, with AlreadyImported populated for the given company.
|
||||
/// Optionally filters by search term, output mode, or industry hint.
|
||||
/// </summary>
|
||||
Task<IEnumerable<FormulaLibraryCardDto>> BrowseAsync(
|
||||
int companyId,
|
||||
string? search = null,
|
||||
string? outputMode = null,
|
||||
string? industryHint = null);
|
||||
|
||||
/// <summary>Full detail for the import preview modal, including field list and formula.</summary>
|
||||
Task<FormulaLibraryDetailDto?> GetDetailAsync(int libraryItemId, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a company template to the community library.
|
||||
/// If the template was previously shared and unpublished, re-publishes the existing row.
|
||||
/// Updates the library entry fields from the current template state on re-share.
|
||||
/// </summary>
|
||||
Task<int> ShareAsync(int companyId, string userId, ShareFormulaRequest request);
|
||||
|
||||
/// <summary>Sets IsPublished = false. Existing imports are unaffected.</summary>
|
||||
Task UnshareAsync(int libraryItemId, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Copies a library entry into the company's local CustomItemTemplate table.
|
||||
/// If the company already has an import record for this entry, returns the existing template id.
|
||||
/// </summary>
|
||||
Task<int> ImportAsync(int libraryItemId, int companyId, string userId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the library status for a given CustomItemTemplate — whether it is shared,
|
||||
/// eligible to be shared, and where it was imported from if applicable.
|
||||
/// </summary>
|
||||
Task<FormulaLibraryStatusDto> GetTemplateLibraryStatusAsync(int templateId, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Nulls out DiagramImagePath on the FormulaLibraryItem and all imported copies
|
||||
/// when a source template's diagram is removed. Call from CompanySettingsController
|
||||
/// when a diagram is deleted or replaced.
|
||||
/// </summary>
|
||||
Task CascadeRemoveDiagramAsync(int sourceCustomItemTemplateId);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using AutoMapper;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Application.DTOs.Company;
|
||||
|
||||
namespace PowderCoating.Application.Mappings;
|
||||
|
||||
public class FormulaLibraryProfile : Profile
|
||||
{
|
||||
public FormulaLibraryProfile()
|
||||
{
|
||||
CreateMap<FormulaLibraryItem, FormulaLibraryCardDto>()
|
||||
.ForMember(dest => dest.InspiredByName,
|
||||
opt => opt.MapFrom(src => src.InspiredBy != null ? src.InspiredBy.Name : null))
|
||||
.ForMember(dest => dest.InspiredByCompanyName,
|
||||
opt => opt.MapFrom(src => src.InspiredBy != null ? src.InspiredBy.SourceCompanyName : null))
|
||||
.ForMember(dest => dest.AlreadyImported, opt => opt.Ignore()); // set by service
|
||||
|
||||
CreateMap<FormulaLibraryItem, FormulaLibraryDetailDto>()
|
||||
.IncludeBase<FormulaLibraryItem, FormulaLibraryCardDto>()
|
||||
.ForMember(dest => dest.FieldCount,
|
||||
opt => opt.MapFrom(src => CountFields(src.FieldsJson)));
|
||||
}
|
||||
|
||||
private static int CountFields(string fieldsJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
var doc = System.Text.Json.JsonDocument.Parse(fieldsJson);
|
||||
return doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Array
|
||||
? doc.RootElement.GetArrayLength()
|
||||
: 0;
|
||||
}
|
||||
catch { return 0; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user