diff --git a/src/PowderCoating.Application/DTOs/Company/CompanySettingsDtos.cs b/src/PowderCoating.Application/DTOs/Company/CompanySettingsDtos.cs
index 01eb350..f8df2c1 100644
--- a/src/PowderCoating.Application/DTOs/Company/CompanySettingsDtos.cs
+++ b/src/PowderCoating.Application/DTOs/Company/CompanySettingsDtos.cs
@@ -112,6 +112,7 @@ namespace PowderCoating.Application.DTOs.Company
// Labor Rates
public decimal StandardLaborRate { get; set; }
+ public decimal? LaborCostPerHour { get; set; }
public decimal AdditionalCoatLaborPercent { get; set; }
// Equipment Operating Costs
@@ -185,6 +186,10 @@ namespace PowderCoating.Application.DTOs.Company
[Display(Name = "Standard Labor Rate ($/hr)")]
public decimal StandardLaborRate { get; set; }
+ [Range(0, 10000, ErrorMessage = "Labor cost rate must be between 0 and 10,000")]
+ [Display(Name = "Shop Labor Cost Rate ($/hr)")]
+ public decimal? LaborCostPerHour { get; set; }
+
[Range(0, 100, ErrorMessage = "Additional coat labor percent must be between 0 and 100")]
[Display(Name = "Additional Coat Labor (%)")]
public decimal AdditionalCoatLaborPercent { get; set; } = 30m;
diff --git a/src/PowderCoating.Application/DTOs/Import/ShopWorkerImportDto.cs b/src/PowderCoating.Application/DTOs/Import/ShopWorkerImportDto.cs
deleted file mode 100644
index 873c3ca..0000000
--- a/src/PowderCoating.Application/DTOs/Import/ShopWorkerImportDto.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using CsvHelper.Configuration.Attributes;
-
-namespace PowderCoating.Application.DTOs.Import;
-
-///
-/// DTO for importing shop workers from CSV files.
-/// Valid Role values: GeneralLabor, Sandblaster, Coater, Masker, QualityControl, OvenOperator, Supervisor, Maintenance
-///
-public class ShopWorkerImportDto
-{
- [Name("Name")]
- public string Name { get; set; } = string.Empty;
-
- [Name("Role")]
- public string Role { get; set; } = "GeneralLabor";
-
- [Name("Phone")]
- public string? Phone { get; set; }
-
- [Name("Email")]
- public string? Email { get; set; }
-
- [Name("IsActive")]
- public bool? IsActive { get; set; }
-
- [Name("Notes")]
- public string? Notes { get; set; }
-}
diff --git a/src/PowderCoating.Application/DTOs/ShopWorker/CreateShopWorkerDto.cs b/src/PowderCoating.Application/DTOs/ShopWorker/CreateShopWorkerDto.cs
deleted file mode 100644
index f577542..0000000
--- a/src/PowderCoating.Application/DTOs/ShopWorker/CreateShopWorkerDto.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-using PowderCoating.Core.Enums;
-
-namespace PowderCoating.Application.DTOs.ShopWorker;
-
-public class CreateShopWorkerDto
-{
- [Required(ErrorMessage = "Worker name is required")]
- [StringLength(100, ErrorMessage = "Name cannot exceed 100 characters")]
- public string Name { get; set; } = string.Empty;
-
- [Required(ErrorMessage = "Role is required")]
- public ShopWorkerRole Role { get; set; } = ShopWorkerRole.GeneralLabor;
-
- [Phone(ErrorMessage = "Invalid phone number format")]
- [StringLength(20, ErrorMessage = "Phone cannot exceed 20 characters")]
- public string? Phone { get; set; }
-
- [EmailAddress(ErrorMessage = "Invalid email address format")]
- [StringLength(100, ErrorMessage = "Email cannot exceed 100 characters")]
- public string? Email { get; set; }
-
- public bool IsActive { get; set; } = true;
-
- [StringLength(500, ErrorMessage = "Notes cannot exceed 500 characters")]
- public string? Notes { get; set; }
-}
diff --git a/src/PowderCoating.Application/DTOs/ShopWorker/ShopWorkerDto.cs b/src/PowderCoating.Application/DTOs/ShopWorker/ShopWorkerDto.cs
deleted file mode 100644
index f9cc0bb..0000000
--- a/src/PowderCoating.Application/DTOs/ShopWorker/ShopWorkerDto.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using PowderCoating.Core.Enums;
-
-namespace PowderCoating.Application.DTOs.ShopWorker;
-
-public class ShopWorkerDto
-{
- public int Id { get; set; }
- public string Name { get; set; } = string.Empty;
- public ShopWorkerRole Role { get; set; }
- public string? Phone { get; set; }
- public string? Email { get; set; }
- public bool IsActive { get; set; }
- public string? Notes { get; set; }
- public DateTime CreatedAt { get; set; }
- public DateTime? UpdatedAt { get; set; }
-}
diff --git a/src/PowderCoating.Application/DTOs/ShopWorker/UpdateShopWorkerDto.cs b/src/PowderCoating.Application/DTOs/ShopWorker/UpdateShopWorkerDto.cs
deleted file mode 100644
index fd5dd71..0000000
--- a/src/PowderCoating.Application/DTOs/ShopWorker/UpdateShopWorkerDto.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-using PowderCoating.Core.Enums;
-
-namespace PowderCoating.Application.DTOs.ShopWorker;
-
-public class UpdateShopWorkerDto
-{
- public int Id { get; set; }
-
- [Required(ErrorMessage = "Worker name is required")]
- [StringLength(100, ErrorMessage = "Name cannot exceed 100 characters")]
- public string Name { get; set; } = string.Empty;
-
- [Required(ErrorMessage = "Role is required")]
- public ShopWorkerRole Role { get; set; }
-
- [Phone(ErrorMessage = "Invalid phone number format")]
- [StringLength(20, ErrorMessage = "Phone cannot exceed 20 characters")]
- public string? Phone { get; set; }
-
- [EmailAddress(ErrorMessage = "Invalid email address format")]
- [StringLength(100, ErrorMessage = "Email cannot exceed 100 characters")]
- public string? Email { get; set; }
-
- public bool IsActive { get; set; }
-
- [StringLength(500, ErrorMessage = "Notes cannot exceed 500 characters")]
- public string? Notes { get; set; }
-}
diff --git a/src/PowderCoating.Application/DTOs/User/UserManagementDtos.cs b/src/PowderCoating.Application/DTOs/User/UserManagementDtos.cs
index 5811974..c1bb039 100644
--- a/src/PowderCoating.Application/DTOs/User/UserManagementDtos.cs
+++ b/src/PowderCoating.Application/DTOs/User/UserManagementDtos.cs
@@ -217,6 +217,10 @@ public class UpdateCompanyUserDto
[Display(Name = "Active")]
public bool IsActive { get; set; }
+ [Range(0, 10000, ErrorMessage = "Labor cost rate must be between 0 and 10,000")]
+ [Display(Name = "Labor Cost Rate ($/hr)")]
+ public decimal? LaborCostPerHour { get; set; }
+
[Required(ErrorMessage = "Hire date is required")]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
diff --git a/src/PowderCoating.Application/Interfaces/ICsvImportService.cs b/src/PowderCoating.Application/Interfaces/ICsvImportService.cs
index 7d27686..0fc3eb6 100644
--- a/src/PowderCoating.Application/Interfaces/ICsvImportService.cs
+++ b/src/PowderCoating.Application/Interfaces/ICsvImportService.cs
@@ -136,18 +136,7 @@ public interface ICsvImportService
///
Task ImportVendorsAsync(Stream csvStream, int companyId);
- ///
- /// Generate a CSV template file for shop worker imports.
- ///
- byte[] GenerateShopWorkerTemplate();
-
- ///
- /// Import shop workers from a CSV stream.
- /// Updates existing workers matched by Name; creates new ones otherwise.
- ///
- Task ImportShopWorkersAsync(Stream csvStream, int companyId);
-
- ///
+///
/// Generate a CSV template file for prep service imports.
///
byte[] GeneratePrepServiceTemplate();
diff --git a/src/PowderCoating.Application/Mappings/JobProfile.cs b/src/PowderCoating.Application/Mappings/JobProfile.cs
index b4415a0..a6d03b3 100644
--- a/src/PowderCoating.Application/Mappings/JobProfile.cs
+++ b/src/PowderCoating.Application/Mappings/JobProfile.cs
@@ -73,7 +73,7 @@ public class JobProfile : Profile
// JobTimeEntry → JobTimeEntryDto
CreateMap()
.ForMember(dest => dest.WorkerName, opt => opt.MapFrom(src =>
- src.UserDisplayName ?? (src.Worker != null ? src.Worker.Name : string.Empty)));
+ src.UserDisplayName ?? string.Empty));
// CreateJobDto to Job
CreateMap()
diff --git a/src/PowderCoating.Application/Mappings/ShopWorkerProfile.cs b/src/PowderCoating.Application/Mappings/ShopWorkerProfile.cs
deleted file mode 100644
index c450636..0000000
--- a/src/PowderCoating.Application/Mappings/ShopWorkerProfile.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using AutoMapper;
-using PowderCoating.Application.DTOs.ShopWorker;
-using PowderCoating.Core.Entities;
-
-namespace PowderCoating.Application.Mappings;
-
-public class ShopWorkerProfile : Profile
-{
- public ShopWorkerProfile()
- {
- // Entity to DTO
- CreateMap();
-
- // DTO to Entity
- CreateMap();
- CreateMap();
-
- // Reverse mappings
- CreateMap();
- CreateMap();
- CreateMap();
- }
-}
diff --git a/src/PowderCoating.Core/Entities/ApplicationUser.cs b/src/PowderCoating.Core/Entities/ApplicationUser.cs
index 06a4a9b..d9500e6 100644
--- a/src/PowderCoating.Core/Entities/ApplicationUser.cs
+++ b/src/PowderCoating.Core/Entities/ApplicationUser.cs
@@ -58,7 +58,14 @@ public class ApplicationUser : IdentityUser
public string? SidebarColor { get; set; } = "ocean";
public string? Notes { get; set; }
-
+
+ ///
+ /// Per-worker labor cost per hour used for job costing profit/margin calculations.
+ /// Overrides the company-level LaborCostPerHour when set.
+ /// Leave null to use the company default.
+ ///
+ public decimal? LaborCostPerHour { get; set; }
+
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
public DateTime? LastLoginDate { get; set; }
diff --git a/src/PowderCoating.Core/Entities/Company.cs b/src/PowderCoating.Core/Entities/Company.cs
index 1d5552c..8d74c17 100644
--- a/src/PowderCoating.Core/Entities/Company.cs
+++ b/src/PowderCoating.Core/Entities/Company.cs
@@ -141,8 +141,7 @@ public class Company : BaseEntity
public virtual ICollection Quotes { get; set; } = new List();
public virtual ICollection InventoryItems { get; set; } = new List();
public virtual ICollection Vendors { get; set; } = new List();
- public virtual ICollection ShopWorkers { get; set; } = new List();
- public virtual ICollection PricingTiers { get; set; } = new List();
+public virtual ICollection PricingTiers { get; set; } = new List();
public virtual CompanyOperatingCosts? OperatingCosts { get; set; }
public virtual CompanyPreferences? Preferences { get; set; }
}
diff --git a/src/PowderCoating.Core/Entities/CompanyOperatingCosts.cs b/src/PowderCoating.Core/Entities/CompanyOperatingCosts.cs
index c9e1e5c..e4cb82e 100644
--- a/src/PowderCoating.Core/Entities/CompanyOperatingCosts.cs
+++ b/src/PowderCoating.Core/Entities/CompanyOperatingCosts.cs
@@ -13,6 +13,14 @@ namespace PowderCoating.Core.Entities
[Range(0, 10000)]
public decimal StandardLaborRate { get; set; }
+ ///
+ /// Actual labor cost per hour (wages + burden) used exclusively for internal job costing and profit/margin display.
+ /// This is NOT the billing rate — it should reflect what you actually pay workers.
+ /// When null, the costing engine defaults to 20% of StandardLaborRate.
+ ///
+ [Range(0, 10000)]
+ public decimal? LaborCostPerHour { get; set; }
+
// Additional Coat Labor Percentage (percentage of base labor for each additional coat beyond the first)
[Range(0, 100)]
public decimal AdditionalCoatLaborPercent { get; set; } = 30m;
diff --git a/src/PowderCoating.Core/Entities/JobTimeEntry.cs b/src/PowderCoating.Core/Entities/JobTimeEntry.cs
index be96cc6..464dee9 100644
--- a/src/PowderCoating.Core/Entities/JobTimeEntry.cs
+++ b/src/PowderCoating.Core/Entities/JobTimeEntry.cs
@@ -3,7 +3,6 @@ namespace PowderCoating.Core.Entities;
public class JobTimeEntry : BaseEntity
{
public int JobId { get; set; }
- public int? ShopWorkerId { get; set; } // legacy — kept for entries created before user migration
public string? UserId { get; set; } // FK to AspNetUsers
public string? UserDisplayName { get; set; } // snapshot of worker name at entry creation time
public DateTime WorkDate { get; set; }
@@ -13,5 +12,4 @@ public class JobTimeEntry : BaseEntity
// Navigation
public virtual Job Job { get; set; } = null!;
- public virtual ShopWorker? Worker { get; set; } // nullable — only populated for legacy entries
}
diff --git a/src/PowderCoating.Core/Entities/ShopWorker.cs b/src/PowderCoating.Core/Entities/ShopWorker.cs
deleted file mode 100644
index 121b07b..0000000
--- a/src/PowderCoating.Core/Entities/ShopWorker.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using PowderCoating.Core.Enums;
-
-namespace PowderCoating.Core.Entities;
-
-public class ShopWorker : BaseEntity
-{
- public string Name { get; set; } = string.Empty;
- public ShopWorkerRole Role { get; set; } = ShopWorkerRole.GeneralLabor;
- public string? Phone { get; set; }
- public string? Email { get; set; }
- public bool IsActive { get; set; } = true;
- public string? Notes { get; set; }
-
- // Relationships
- public virtual ICollection AssignedJobs { get; set; } = new List();
- public virtual ICollection AssignedMaintenanceTasks { get; set; } = new List();
- public virtual ICollection TimeEntries { get; set; } = new List();
-}
diff --git a/src/PowderCoating.Core/Entities/ShopWorkerRoleCost.cs b/src/PowderCoating.Core/Entities/ShopWorkerRoleCost.cs
deleted file mode 100644
index 050fe2d..0000000
--- a/src/PowderCoating.Core/Entities/ShopWorkerRoleCost.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using PowderCoating.Core.Enums;
-
-namespace PowderCoating.Core.Entities;
-
-///
-/// Optional per-role labor cost rate for job costing / profitability calculations.
-/// If no rate is set for a role, the company's StandardLaborRate is used as fallback.
-///
-public class ShopWorkerRoleCost : BaseEntity
-{
- public ShopWorkerRole Role { get; set; }
-
- /// Cost (pay rate) per hour for this role — used in job costing, NOT billing.
- public decimal HourlyRate { get; set; }
-}
diff --git a/src/PowderCoating.Core/Enums/Enums.cs b/src/PowderCoating.Core/Enums/Enums.cs
index d06dd53..e80d913 100644
--- a/src/PowderCoating.Core/Enums/Enums.cs
+++ b/src/PowderCoating.Core/Enums/Enums.cs
@@ -78,17 +78,6 @@ public enum EquipmentStatus
Retired = 4
}
-public enum ShopWorkerRole
-{
- GeneralLabor = 0,
- Sandblaster = 1,
- Coater = 2,
- Masker = 3,
- QualityControl = 4,
- OvenOperator = 5,
- Supervisor = 6,
- Maintenance = 7
-}
public enum JobPhotoType
{
diff --git a/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs b/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs
index 791d852..8b417a9 100644
--- a/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs
+++ b/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs
@@ -54,9 +54,7 @@ public interface IUnitOfWork : IDisposable
IRepository AppointmentStatusLookups { get; }
IRepository AppointmentTypeLookups { get; }
IRepository PrepServices { get; }
- IRepository ShopWorkers { get; }
- IRepository ShopWorkerRoleCosts { get; }
- IRepository ReworkRecords { get; }
+IRepository ReworkRecords { get; }
IRepository Refunds { get; }
IRepository CreditMemos { get; }
IRepository CreditMemoApplications { get; }
diff --git a/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs b/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs
index b4482ae..a4d25be 100644
--- a/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs
+++ b/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs
@@ -205,11 +205,7 @@ public class ApplicationDbContext : IdentityDbContext, IDataPro
public DbSet MaintenanceRecords { get; set; }
/// Supplier/vendor records used by Purchasing and Accounts Payable; tenant-filtered with soft delete.
public DbSet Vendors { get; set; }
- /// Shop worker profiles with role assignments; tenant-filtered with soft delete.
- public DbSet ShopWorkers { get; set; }
- /// Per-role labour cost rates used in pricing calculations; unique index on (CompanyId, Role).
- public DbSet ShopWorkerRoleCosts { get; set; }
- /// Rework records tracking quality failures and remediation work against a job; tenant-filtered with soft delete.
+/// Rework records tracking quality failures and remediation work against a job; tenant-filtered with soft delete.
public DbSet ReworkRecords { get; set; }
/// Customer refund records; tenant-filtered with soft delete.
public DbSet Refunds { get; set; }
@@ -530,11 +526,7 @@ public class ApplicationDbContext : IdentityDbContext, IDataPro
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
modelBuilder.Entity().HasQueryFilter(e =>
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
- modelBuilder.Entity().HasQueryFilter(e =>
- !e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
- modelBuilder.Entity().HasQueryFilter(e =>
- !e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
- modelBuilder.Entity().HasQueryFilter(e =>
+modelBuilder.Entity().HasQueryFilter(e =>
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
modelBuilder.Entity().HasQueryFilter(e =>
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
@@ -1314,12 +1306,7 @@ public class ApplicationDbContext : IdentityDbContext, IDataPro
.HasForeignKey(m => m.PerformedById)
.OnDelete(DeleteBehavior.SetNull);
- // ShopWorker relationships
- modelBuilder.Entity()
- .HasOne()
- .WithMany(c => c.ShopWorkers)
- .HasForeignKey(e => e.CompanyId)
- .OnDelete(DeleteBehavior.Restrict);
+
modelBuilder.Entity()
.HasOne(j => j.AssignedUser)
@@ -1393,10 +1380,7 @@ public class ApplicationDbContext : IdentityDbContext, IDataPro
modelBuilder.Entity()
.HasIndex(p => p.CompanyId);
- modelBuilder.Entity()
- .HasIndex(w => w.CompanyId);
-
- modelBuilder.Entity()
+modelBuilder.Entity()
.HasIndex(c => c.CompanyId);
modelBuilder.Entity()
@@ -1431,12 +1415,7 @@ public class ApplicationDbContext : IdentityDbContext, IDataPro
.IsUnique()
.HasDatabaseName("IX_Jobs_CompanyId_JobNumber");
- modelBuilder.Entity()
- .HasIndex(r => new { r.CompanyId, r.Role })
- .IsUnique()
- .HasDatabaseName("IX_ShopWorkerRoleCosts_CompanyId_Role");
-
- modelBuilder.Entity()
+modelBuilder.Entity()
.Property(j => j.ShopAccessCode)
.HasDefaultValueSql("NEWID()");
diff --git a/src/PowderCoating.Infrastructure/Data/AuditInterceptor.cs b/src/PowderCoating.Infrastructure/Data/AuditInterceptor.cs
index cf256e6..78f3800 100644
--- a/src/PowderCoating.Infrastructure/Data/AuditInterceptor.cs
+++ b/src/PowderCoating.Infrastructure/Data/AuditInterceptor.cs
@@ -21,7 +21,7 @@ public class AuditInterceptor : SaveChangesInterceptor
private static readonly HashSet AuditedTypes = new(StringComparer.Ordinal)
{
nameof(Customer), nameof(Job), nameof(Quote), nameof(Equipment),
- nameof(MaintenanceRecord), nameof(Vendor), nameof(ShopWorker),
+ nameof(MaintenanceRecord), nameof(Vendor),
nameof(InventoryItem), nameof(Company),
// Financial entities
nameof(Invoice), nameof(Payment), nameof(Bill), nameof(BillPayment),
diff --git a/src/PowderCoating.Infrastructure/Migrations/20260515234413_AddLaborCostPerHour.Designer.cs b/src/PowderCoating.Infrastructure/Migrations/20260515234413_AddLaborCostPerHour.Designer.cs
new file mode 100644
index 0000000..4e9cea8
--- /dev/null
+++ b/src/PowderCoating.Infrastructure/Migrations/20260515234413_AddLaborCostPerHour.Designer.cs
@@ -0,0 +1,10784 @@
+//
+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("20260515234413_AddLaborCostPerHour")]
+ partial class AddLaborCostPerHour
+ {
+ ///
+ 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("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