diff --git a/src/PowderCoating.Application/DTOs/Company/FormulaLibraryDtos.cs b/src/PowderCoating.Application/DTOs/Company/FormulaLibraryDtos.cs
new file mode 100644
index 0000000..3715312
--- /dev/null
+++ b/src/PowderCoating.Application/DTOs/Company/FormulaLibraryDtos.cs
@@ -0,0 +1,69 @@
+namespace PowderCoating.Application.DTOs.Company;
+
+// ── Browse / card display ──────────────────────────────────────────────────
+
+/// Lean DTO for the community library browse grid card.
+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; }
+
+ /// Non-null when this formula was derived from another library entry.
+ public int? InspiredByFormulaLibraryItemId { get; set; }
+ public string? InspiredByName { get; set; }
+ public string? InspiredByCompanyName { get; set; }
+
+ /// True when the current company has already imported this entry.
+ public bool AlreadyImported { get; set; }
+
+ /// True when this formula was shared by the current browsing company.
+ public bool IsOwnFormula { get; set; }
+}
+
+// ── Full detail (import preview modal) ────────────────────────────────────
+
+/// Full DTO used in the import preview modal — shows fields and formula.
+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 ───────────────────────────────────────────
+
+/// Submitted when a company admin shares one of their templates to the community library.
+public class ShareFormulaRequest
+{
+ public int CustomItemTemplateId { get; set; }
+ public string? Tags { get; set; }
+ public string? IndustryHint { get; set; }
+}
+
+// ── Company Settings list view ─────────────────────────────────────────────
+
+/// Status of a template relative to the community library, shown in Company Settings.
+public class FormulaLibraryStatusDto
+{
+ /// The FormulaLibraryItem Id, if this template has ever been shared.
+ public int? LibraryItemId { get; set; }
+ public bool IsPublished { get; set; }
+
+ /// Whether this template is eligible to be shared (original or modified import).
+ public bool CanShare { get; set; }
+
+ /// Set when this template was imported; the name of the original library entry.
+ public string? ImportedFromName { get; set; }
+ public string? ImportedFromCompany { get; set; }
+}
diff --git a/src/PowderCoating.Application/Interfaces/IFormulaLibraryService.cs b/src/PowderCoating.Application/Interfaces/IFormulaLibraryService.cs
new file mode 100644
index 0000000..bf9fe7e
--- /dev/null
+++ b/src/PowderCoating.Application/Interfaces/IFormulaLibraryService.cs
@@ -0,0 +1,51 @@
+using PowderCoating.Application.DTOs.Company;
+
+namespace PowderCoating.Application.Interfaces;
+
+///
+/// Manages the community formula library: sharing, unsharing, importing, and browsing.
+///
+public interface IFormulaLibraryService
+{
+ ///
+ /// Returns all published library entries, with AlreadyImported populated for the given company.
+ /// Optionally filters by search term, output mode, or industry hint.
+ ///
+ Task> BrowseAsync(
+ int companyId,
+ string? search = null,
+ string? outputMode = null,
+ string? industryHint = null);
+
+ /// Full detail for the import preview modal, including field list and formula.
+ Task GetDetailAsync(int libraryItemId, int companyId);
+
+ ///
+ /// 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.
+ ///
+ Task ShareAsync(int companyId, string userId, ShareFormulaRequest request);
+
+ /// Sets IsPublished = false. Existing imports are unaffected.
+ Task UnshareAsync(int libraryItemId, int companyId);
+
+ ///
+ /// 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.
+ ///
+ Task ImportAsync(int libraryItemId, int companyId, string userId);
+
+ ///
+ /// Returns the library status for a given CustomItemTemplate — whether it is shared,
+ /// eligible to be shared, and where it was imported from if applicable.
+ ///
+ Task GetTemplateLibraryStatusAsync(int templateId, int companyId);
+
+ ///
+ /// 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.
+ ///
+ Task CascadeRemoveDiagramAsync(int sourceCustomItemTemplateId);
+}
diff --git a/src/PowderCoating.Application/Mappings/FormulaLibraryProfile.cs b/src/PowderCoating.Application/Mappings/FormulaLibraryProfile.cs
new file mode 100644
index 0000000..e5ca5a2
--- /dev/null
+++ b/src/PowderCoating.Application/Mappings/FormulaLibraryProfile.cs
@@ -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()
+ .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()
+ .IncludeBase()
+ .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; }
+ }
+}
diff --git a/src/PowderCoating.Core/Entities/CustomItemTemplate.cs b/src/PowderCoating.Core/Entities/CustomItemTemplate.cs
index cb5830b..e8db2d4 100644
--- a/src/PowderCoating.Core/Entities/CustomItemTemplate.cs
+++ b/src/PowderCoating.Core/Entities/CustomItemTemplate.cs
@@ -35,4 +35,19 @@ public class CustomItemTemplate : BaseEntity
/// Path format: {companyId}/{templateId}/diagram.{ext}
///
public string? DiagramImagePath { get; set; }
+
+ // ── Community library tracking ─────────────────────────────────────────
+
+ ///
+ /// Set when this template was imported from the community library.
+ /// Null for originally created templates.
+ ///
+ public int? SourceFormulaLibraryItemId { get; set; }
+ public virtual FormulaLibraryItem? SourceFormulaLibraryItem { get; set; }
+
+ ///
+ /// True once the user edits an imported template. Only modified imports (and original
+ /// creations) are eligible to be shared back to the community library.
+ ///
+ public bool IsModifiedFromSource { get; set; }
}
diff --git a/src/PowderCoating.Core/Entities/FormulaLibraryImport.cs b/src/PowderCoating.Core/Entities/FormulaLibraryImport.cs
new file mode 100644
index 0000000..b065602
--- /dev/null
+++ b/src/PowderCoating.Core/Entities/FormulaLibraryImport.cs
@@ -0,0 +1,19 @@
+namespace PowderCoating.Core.Entities;
+
+///
+/// Records that a company imported a specific FormulaLibraryItem into their local template library.
+/// Tenant-scoped via BaseEntity.CompanyId. One row per (company, library item) — re-importing the
+/// same item overwrites the existing row rather than creating a duplicate.
+///
+public class FormulaLibraryImport : BaseEntity
+{
+ public int FormulaLibraryItemId { get; set; }
+ public virtual FormulaLibraryItem FormulaLibraryItem { get; set; } = null!;
+
+ public string ImportedByUserId { get; set; } = string.Empty;
+ public DateTime ImportedAt { get; set; } = DateTime.UtcNow;
+
+ /// The CustomItemTemplate row created in this company's local library on import.
+ public int ResultingCustomItemTemplateId { get; set; }
+ public virtual CustomItemTemplate ResultingCustomItemTemplate { get; set; } = null!;
+}
diff --git a/src/PowderCoating.Core/Entities/FormulaLibraryItem.cs b/src/PowderCoating.Core/Entities/FormulaLibraryItem.cs
new file mode 100644
index 0000000..06b2611
--- /dev/null
+++ b/src/PowderCoating.Core/Entities/FormulaLibraryItem.cs
@@ -0,0 +1,70 @@
+namespace PowderCoating.Core.Entities;
+
+///
+/// Platform-level community library entry for a shared custom formula template.
+/// Not tenant-scoped — no BaseEntity, no CompanyId, no soft delete.
+/// Shared voluntarily by the originating company; imported as independent copies by others.
+///
+public class FormulaLibraryItem
+{
+ public int Id { get; set; }
+
+ // ── Formula content (copied from CustomItemTemplate at share time) ─────
+
+ public string Name { get; set; } = string.Empty;
+ public string? Description { get; set; }
+
+ /// "FixedRate" or "SurfaceAreaSqFt" — mirrors CustomItemTemplate.OutputMode.
+ public string OutputMode { get; set; } = "FixedRate";
+
+ /// JSON array of field definitions: [{name, label, unit, defaultValue}]
+ public string FieldsJson { get; set; } = "[]";
+
+ /// NCalc expression using field name slugs and the reserved variable 'rate'.
+ public string Formula { get; set; } = string.Empty;
+
+ public decimal? DefaultRate { get; set; }
+ public string? RateLabel { get; set; }
+ public string? Notes { get; set; }
+
+ ///
+ /// Blob path referencing the source template's diagram image.
+ /// Nulled out (here and on all imports) if the source template's diagram is removed.
+ ///
+ public string? DiagramImagePath { get; set; }
+
+ // ── Attribution ────────────────────────────────────────────────────────
+
+ /// Comma-separated community tags, e.g. "HVAC,Sheet Metal".
+ public string? Tags { get; set; }
+
+ /// Optional industry hint shown on the browse card, e.g. "HVAC", "Automotive".
+ public string? IndustryHint { get; set; }
+
+ /// Id of the CustomItemTemplate this was shared from.
+ public int SourceCustomItemTemplateId { get; set; }
+
+ public int SourceCompanyId { get; set; }
+
+ /// Denormalized company name so it renders without a join when the company is gone.
+ public string SourceCompanyName { get; set; } = string.Empty;
+
+ ///
+ /// When non-null, this entry was derived from an imported formula that was subsequently
+ /// modified. Points to the original library entry. Shown as "Inspired by..." on the browse card.
+ ///
+ public int? InspiredByFormulaLibraryItemId { get; set; }
+ public virtual FormulaLibraryItem? InspiredBy { get; set; }
+
+ public string SharedByUserId { get; set; } = string.Empty;
+ public DateTime SharedAt { get; set; } = DateTime.UtcNow;
+
+ /// False when the creator has removed it from the community library.
+ public bool IsPublished { get; set; } = true;
+
+ /// Running count of how many companies have imported this entry.
+ public int ImportCount { get; set; }
+
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+ public DateTime? UpdatedAt { get; set; }
+}
diff --git a/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs b/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs
index 37687db..4982c5b 100644
--- a/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs
+++ b/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs
@@ -158,6 +158,10 @@ IRepository ReworkRecords { get; }
// Custom Formula Templates
IRepository CustomItemTemplates { get; }
+ // Formula Community Library
+ IPlainRepository FormulaLibrary { get; }
+ IRepository FormulaLibraryImports { get; }
+
// Employee Timeclock
IRepository EmployeeClockEntries { get; }
IRepository TimeclockKioskDevices { get; }
diff --git a/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs b/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs
index d30ce26..8be0ebf 100644
--- a/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs
+++ b/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs
@@ -289,6 +289,12 @@ public class ApplicationDbContext : IdentityDbContext, IDataPro
///
public DbSet PowderCatalogItems { get; set; }
+ /// Community library of shared formula templates. Platform-level, no tenant filter.
+ public DbSet FormulaLibraryItems { get; set; }
+
+ /// Per-company record of which community library formulas a company has imported.
+ public DbSet FormulaLibraryImports { get; set; }
+
/// User-submitted bug reports; tenant-filtered with soft delete.
public DbSet BugReports { get; set; }
/// File attachments for bug reports; soft-delete only (no tenant filter — access controlled via parent BugReport).
@@ -2074,6 +2080,49 @@ modelBuilder.Entity()
.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()
+ .HasOne(f => f.InspiredBy)
+ .WithMany()
+ .HasForeignKey(f => f.InspiredByFormulaLibraryItemId)
+ .IsRequired(false)
+ .OnDelete(DeleteBehavior.NoAction);
+
+ modelBuilder.Entity()
+ .HasIndex(f => f.SourceCompanyId)
+ .HasDatabaseName("IX_FormulaLibraryItems_SourceCompanyId");
+
+ modelBuilder.Entity()
+ .HasIndex(f => f.IsPublished)
+ .HasDatabaseName("IX_FormulaLibraryItems_IsPublished");
+
+ // FormulaLibraryImport — tenant-scoped; unique per (CompanyId, FormulaLibraryItemId)
+ modelBuilder.Entity()
+ .HasOne(i => i.FormulaLibraryItem)
+ .WithMany()
+ .HasForeignKey(i => i.FormulaLibraryItemId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ modelBuilder.Entity()
+ .HasOne(i => i.ResultingCustomItemTemplate)
+ .WithMany()
+ .HasForeignKey(i => i.ResultingCustomItemTemplateId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ modelBuilder.Entity()
+ .HasIndex(i => new { i.CompanyId, i.FormulaLibraryItemId })
+ .IsUnique()
+ .HasDatabaseName("IX_FormulaLibraryImports_Company_Item");
+
+ // CustomItemTemplate → FormulaLibraryItem (nullable; only set on imported templates)
+ modelBuilder.Entity()
+ .HasOne(t => t.SourceFormulaLibraryItem)
+ .WithMany()
+ .HasForeignKey(t => t.SourceFormulaLibraryItemId)
+ .IsRequired(false)
+ .OnDelete(DeleteBehavior.SetNull);
}
///
diff --git a/src/PowderCoating.Infrastructure/Migrations/20260528010119_AddFormulaLibrary.Designer.cs b/src/PowderCoating.Infrastructure/Migrations/20260528010119_AddFormulaLibrary.Designer.cs
new file mode 100644
index 0000000..e754d3c
--- /dev/null
+++ b/src/PowderCoating.Infrastructure/Migrations/20260528010119_AddFormulaLibrary.Designer.cs
@@ -0,0 +1,11119 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using PowderCoating.Infrastructure.Data;
+
+#nullable disable
+
+namespace PowderCoating.Infrastructure.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20260528010119_AddFormulaLibrary")]
+ partial class AddFormulaLibrary
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("FriendlyName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Xml")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("DataProtectionKeys");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex")
+ .HasFilter("[NormalizedName] IS NOT NULL");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderKey")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("RoleId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Value")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AccountNumber")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AccountSubType")
+ .HasColumnType("int");
+
+ b.Property("AccountType")
+ .HasColumnType("int");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CurrentBalance")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("DeletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeletedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("IsSystem")
+ .HasColumnType("bit");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("OpeningBalance")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("OpeningBalanceDate")
+ .HasColumnType("datetime2");
+
+ b.Property("ParentAccountId")
+ .HasColumnType("int");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentAccountId");
+
+ b.ToTable("Accounts");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.AiItemPrediction", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AiTags")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("Confidence")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ConversationRounds")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DeletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeletedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("PredictedComplexity")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PredictedMinutes")
+ .HasColumnType("int");
+
+ b.Property("PredictedSurfaceAreaSqFt")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("PredictedUnitPrice")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("Reasoning")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserOverrodeEstimate")
+ .HasColumnType("bit");
+
+ b.HasKey("Id");
+
+ b.ToTable("AiItemPredictions");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.AiUsageLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CalledAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("Feature")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("InputLength")
+ .HasColumnType("int");
+
+ b.Property("Success")
+ .HasColumnType("bit");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CompanyId", "CalledAt")
+ .HasDatabaseName("IX_AiUsageLogs_CompanyId_CalledAt");
+
+ b.ToTable("AiUsageLogs");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.Announcement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedByUserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedByUserName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsDismissible")
+ .HasColumnType("bit");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("StartsAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Target")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TargetCompanyId")
+ .HasColumnType("int");
+
+ b.Property("TargetPlan")
+ .HasColumnType("int");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.ToTable("Announcements");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.AnnouncementDismissal", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AnnouncementId")
+ .HasColumnType("int");
+
+ b.Property("DismissedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AnnouncementId", "UserId")
+ .IsUnique();
+
+ b.ToTable("AnnouncementDismissals");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("Address")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("BanReason")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("BannedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("BannedByUserId")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CanApproveQuotes")
+ .HasColumnType("bit");
+
+ b.Property("CanCreateQuotes")
+ .HasColumnType("bit");
+
+ b.Property("CanManageAccounting")
+ .HasColumnType("bit");
+
+ b.Property("CanManageBills")
+ .HasColumnType("bit");
+
+ b.Property("CanManageCalendar")
+ .HasColumnType("bit");
+
+ b.Property("CanManageCustomers")
+ .HasColumnType("bit");
+
+ b.Property("CanManageEquipment")
+ .HasColumnType("bit");
+
+ b.Property("CanManageInventory")
+ .HasColumnType("bit");
+
+ b.Property("CanManageInvoices")
+ .HasColumnType("bit");
+
+ b.Property("CanManageJobs")
+ .HasColumnType("bit");
+
+ b.Property("CanManageMaintenance")
+ .HasColumnType("bit");
+
+ b.Property("CanManageProducts")
+ .HasColumnType("bit");
+
+ b.Property("CanManageVendors")
+ .HasColumnType("bit");
+
+ b.Property("CanViewCalendar")
+ .HasColumnType("bit");
+
+ b.Property("CanViewProducts")
+ .HasColumnType("bit");
+
+ b.Property("CanViewReports")
+ .HasColumnType("bit");
+
+ b.Property("CanViewShopFloor")
+ .HasColumnType("bit");
+
+ b.Property("City")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("CompanyRole")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DashboardLayout")
+ .HasColumnType("int");
+
+ b.Property("DateFormat")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Department")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("EmployeeNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("HireDate")
+ .HasColumnType("datetime2");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsBanned")
+ .HasColumnType("bit");
+
+ b.Property("KioskPin")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("LaborCostPerHour")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("LastLoginDate")
+ .HasColumnType("datetime2");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("Notes")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PasskeyPromptDismissed")
+ .HasColumnType("bit");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("Position")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ProfilePictureFilePath")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SidebarColor")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("State")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TerminationDate")
+ .HasColumnType("datetime2");
+
+ b.Property("Theme")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TimeZone")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("ZipCode")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CompanyId");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.Appointment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ActualEndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("ActualStartTime")
+ .HasColumnType("datetime2");
+
+ b.Property("AppointmentNumber")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AppointmentStatusId")
+ .HasColumnType("int");
+
+ b.Property("AppointmentTypeId")
+ .HasColumnType("int");
+
+ b.Property("AssignedUserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CustomerId")
+ .HasColumnType("int");
+
+ b.Property("DeletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeletedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsAllDay")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("IsReminderEnabled")
+ .HasColumnType("bit");
+
+ b.Property("JobId")
+ .HasColumnType("int");
+
+ b.Property("Location")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Notes")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ReminderMinutesBefore")
+ .HasColumnType("int");
+
+ b.Property("ReminderSentAt")
+ .HasColumnType("datetime2");
+
+ b.Property("ScheduledEndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("ScheduledStartTime")
+ .HasColumnType("datetime2");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppointmentStatusId");
+
+ b.HasIndex("AppointmentTypeId");
+
+ b.HasIndex("AssignedUserId");
+
+ b.HasIndex("CustomerId");
+
+ b.HasIndex("JobId");
+
+ b.HasIndex("ScheduledStartTime");
+
+ b.HasIndex("CompanyId", "AppointmentStatusId")
+ .HasDatabaseName("IX_Appointments_CompanyId_AppointmentStatusId");
+
+ b.HasIndex("CompanyId", "ScheduledStartTime")
+ .HasDatabaseName("IX_Appointments_CompanyId_ScheduledStartTime");
+
+ b.ToTable("Appointments");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.AppointmentStatusLookup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ColorClass")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DeletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeletedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DisplayOrder")
+ .HasColumnType("int");
+
+ b.Property("IconClass")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("IsSystemDefined")
+ .HasColumnType("bit");
+
+ b.Property("IsTerminalStatus")
+ .HasColumnType("bit");
+
+ b.Property("StatusCode")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("AppointmentStatusLookups");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.AppointmentTypeLookup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ColorClass")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DeletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeletedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DisplayOrder")
+ .HasColumnType("int");
+
+ b.Property("IconClass")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property