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:
2026-05-27 21:54:51 -04:00
parent 32d09b38f1
commit ca7e905832
24 changed files with 12959 additions and 10 deletions
@@ -289,6 +289,12 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
/// </summary>
public DbSet<PowderCatalogItem> PowderCatalogItems { get; set; }
/// <summary>Community library of shared formula templates. Platform-level, no tenant filter.</summary>
public DbSet<FormulaLibraryItem> FormulaLibraryItems { get; set; }
/// <summary>Per-company record of which community library formulas a company has imported.</summary>
public DbSet<FormulaLibraryImport> FormulaLibraryImports { get; set; }
/// <summary>User-submitted bug reports; tenant-filtered with soft delete.</summary>
public DbSet<BugReport> BugReports { get; set; }
/// <summary>File attachments for bug reports; soft-delete only (no tenant filter — access controlled via parent BugReport).</summary>
@@ -2074,6 +2080,49 @@ modelBuilder.Entity<Job>()
.HasIndex(c => new { c.CompanyId, c.MemoNumber })
.IsUnique()
.HasDatabaseName("IX_CreditMemos_CompanyId_MemoNumber");
// FormulaLibraryItem — platform-level, no tenant filter, no soft delete
// Self-referential "Inspired by" FK uses NoAction; cascade nullification handled in service.
modelBuilder.Entity<FormulaLibraryItem>()
.HasOne(f => f.InspiredBy)
.WithMany()
.HasForeignKey(f => f.InspiredByFormulaLibraryItemId)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<FormulaLibraryItem>()
.HasIndex(f => f.SourceCompanyId)
.HasDatabaseName("IX_FormulaLibraryItems_SourceCompanyId");
modelBuilder.Entity<FormulaLibraryItem>()
.HasIndex(f => f.IsPublished)
.HasDatabaseName("IX_FormulaLibraryItems_IsPublished");
// FormulaLibraryImport — tenant-scoped; unique per (CompanyId, FormulaLibraryItemId)
modelBuilder.Entity<FormulaLibraryImport>()
.HasOne(i => i.FormulaLibraryItem)
.WithMany()
.HasForeignKey(i => i.FormulaLibraryItemId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<FormulaLibraryImport>()
.HasOne(i => i.ResultingCustomItemTemplate)
.WithMany()
.HasForeignKey(i => i.ResultingCustomItemTemplateId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<FormulaLibraryImport>()
.HasIndex(i => new { i.CompanyId, i.FormulaLibraryItemId })
.IsUnique()
.HasDatabaseName("IX_FormulaLibraryImports_Company_Item");
// CustomItemTemplate → FormulaLibraryItem (nullable; only set on imported templates)
modelBuilder.Entity<CustomItemTemplate>()
.HasOne(t => t.SourceFormulaLibraryItem)
.WithMany()
.HasForeignKey(t => t.SourceFormulaLibraryItemId)
.IsRequired(false)
.OnDelete(DeleteBehavior.SetNull);
}
/// <summary>