diff --git a/src/PowderCoating.Application/DTOs/Company/CompanySettingsDtos.cs b/src/PowderCoating.Application/DTOs/Company/CompanySettingsDtos.cs
index 52657e7..72b0f37 100644
--- a/src/PowderCoating.Application/DTOs/Company/CompanySettingsDtos.cs
+++ b/src/PowderCoating.Application/DTOs/Company/CompanySettingsDtos.cs
@@ -140,6 +140,14 @@ namespace PowderCoating.Application.DTOs.Company
public decimal DerivedBlastRateSqFtPerHour { get; set; }
/// Derived coating rate — shown to the user as a sanity-check value.
public decimal DerivedCoatingRateSqFtPerHour { get; set; }
+
+ // Facility Overhead
+ public decimal MonthlyRent { get; set; }
+ public decimal MonthlyUtilities { get; set; }
+ public int MonthlyBillableHours { get; set; } = 160;
+
+ /// Derived facility overhead rate = (MonthlyRent + MonthlyUtilities) / MonthlyBillableHours.
+ public decimal FacilityOverheadRatePerHour { get; set; }
}
///
@@ -228,6 +236,19 @@ namespace PowderCoating.Application.DTOs.Company
[Range(0, 500)]
public decimal ComplexityExtremePercent { get; set; } = 25m;
+
+ // Facility Overhead
+ [Range(0, 1000000, ErrorMessage = "Monthly rent must be between 0 and 1,000,000")]
+ [Display(Name = "Monthly Rent ($)")]
+ public decimal MonthlyRent { get; set; } = 0m;
+
+ [Range(0, 1000000, ErrorMessage = "Monthly utilities must be between 0 and 1,000,000")]
+ [Display(Name = "Monthly Utilities ($)")]
+ public decimal MonthlyUtilities { get; set; } = 0m;
+
+ [Range(1, 10000, ErrorMessage = "Billable hours must be between 1 and 10,000")]
+ [Display(Name = "Billable Hours/Month")]
+ public int MonthlyBillableHours { get; set; } = 160;
}
/// DTO for updating the company AI profile text used for AI Photo Quote calibration.
diff --git a/src/PowderCoating.Application/DTOs/Quote/QuoteDtos.cs b/src/PowderCoating.Application/DTOs/Quote/QuoteDtos.cs
index f40ec49..b061994 100644
--- a/src/PowderCoating.Application/DTOs/Quote/QuoteDtos.cs
+++ b/src/PowderCoating.Application/DTOs/Quote/QuoteDtos.cs
@@ -606,6 +606,9 @@ public class QuotePricingBreakdownDto
public int OvenBatches { get; set; }
public int OvenCycleMinutes { get; set; }
+ public decimal FacilityOverheadCost { get; set; }
+ public decimal FacilityOverheadRatePerHour { get; set; }
+
public decimal Total { get; set; }
// Cost Breakdown Details
@@ -822,6 +825,10 @@ public class QuotePricingResult
public int OvenBatches { get; set; }
public int OvenCycleMinutes { get; set; }
+ // Facility overhead (rent + utilities apportioned by estimated job hours)
+ public decimal FacilityOverheadCost { get; set; }
+ public decimal FacilityOverheadRatePerHour { get; set; }
+
// Detailed breakdown for transparency
public decimal MaterialCosts { get; set; }
public decimal LaborCosts { get; set; }
diff --git a/src/PowderCoating.Application/DTOs/Wizard/WizardDtos.cs b/src/PowderCoating.Application/DTOs/Wizard/WizardDtos.cs
index 405f8b8..dfb9b58 100644
--- a/src/PowderCoating.Application/DTOs/Wizard/WizardDtos.cs
+++ b/src/PowderCoating.Application/DTOs/Wizard/WizardDtos.cs
@@ -112,6 +112,19 @@ public class WizardStep2Dto
[Display(Name = "Shop Capability Tier")]
public ShopCapabilityTier ShopCapabilityTier { get; set; } = ShopCapabilityTier.Small;
+
+ // Facility Overhead
+ [Range(0, 1000000)]
+ [Display(Name = "Monthly Rent ($)")]
+ public decimal MonthlyRent { get; set; } = 0m;
+
+ [Range(0, 1000000)]
+ [Display(Name = "Monthly Utilities ($)")]
+ public decimal MonthlyUtilities { get; set; } = 0m;
+
+ [Range(1, 10000)]
+ [Display(Name = "Billable Hours/Month")]
+ public int MonthlyBillableHours { get; set; } = 160;
}
// ─── Step 3: Branding & Numbering ───────────────────────────────────────────
diff --git a/src/PowderCoating.Application/Mappings/CompanyProfile.cs b/src/PowderCoating.Application/Mappings/CompanyProfile.cs
index 52ad975..0bf51db 100644
--- a/src/PowderCoating.Application/Mappings/CompanyProfile.cs
+++ b/src/PowderCoating.Application/Mappings/CompanyProfile.cs
@@ -37,7 +37,11 @@ public class CompanyProfile : Profile
.ForMember(dest => dest.DerivedBlastRateSqFtPerHour,
opt => opt.MapFrom(src => ShopCapabilityCalculator.GetBlastRateSqFtPerHour(src)))
.ForMember(dest => dest.DerivedCoatingRateSqFtPerHour,
- opt => opt.MapFrom(src => ShopCapabilityCalculator.GetCoatingRateSqFtPerHour(src)));
+ opt => opt.MapFrom(src => ShopCapabilityCalculator.GetCoatingRateSqFtPerHour(src)))
+ .ForMember(dest => dest.FacilityOverheadRatePerHour,
+ opt => opt.MapFrom(src => src.MonthlyBillableHours > 0
+ ? (src.MonthlyRent + src.MonthlyUtilities) / src.MonthlyBillableHours
+ : 0m));
CreateMap();
CreateMap();
diff --git a/src/PowderCoating.Application/Services/PricingCalculationService.cs b/src/PowderCoating.Application/Services/PricingCalculationService.cs
index 0705749..226aaa0 100644
--- a/src/PowderCoating.Application/Services/PricingCalculationService.cs
+++ b/src/PowderCoating.Application/Services/PricingCalculationService.cs
@@ -542,6 +542,8 @@ public class PricingCalculationService : IPricingCalculationService
ItemsSubtotal = 0,
ShopSuppliesAmount = 0,
ShopSuppliesPercent = 0,
+ FacilityOverheadCost = 0,
+ FacilityOverheadRatePerHour = 0,
OverheadCosts = 0,
OverheadPercent = 0,
ProfitMargin = 0,
@@ -683,14 +685,27 @@ public class PricingCalculationService : IPricingCalculationService
var itemsAndOvenSubtotal = itemsSubtotal + ovenBatchCost;
- // 5. SHOP SUPPLIES (percentage of items + oven subtotal)
+ // 5. FACILITY OVERHEAD (rent + utilities apportioned by estimated job hours)
+ var facilityOverheadRatePerHour = 0m;
+ var facilityOverheadCost = 0m;
+ if (costs.MonthlyBillableHours > 0 && (costs.MonthlyRent + costs.MonthlyUtilities) > 0)
+ {
+ facilityOverheadRatePerHour = (costs.MonthlyRent + costs.MonthlyUtilities) / costs.MonthlyBillableHours;
+ var totalEstimatedMinutes = items.Sum(i => (decimal)i.EstimatedMinutes * i.Quantity);
+ facilityOverheadCost = facilityOverheadRatePerHour * (totalEstimatedMinutes / 60m);
+ _logger.LogInformation(
+ "Facility overhead: ${Rate:F2}/hr × {Min:F0} min = ${Cost:F2}",
+ facilityOverheadRatePerHour, totalEstimatedMinutes, facilityOverheadCost);
+ }
+
+ // 6. SHOP SUPPLIES (percentage of items + oven subtotal — does not include facility overhead)
var shopSuppliesPercent = costs.ShopSuppliesRate;
var shopSuppliesAmount = itemsAndOvenSubtotal * (shopSuppliesPercent / 100m);
- // 6. SUBTOTAL BEFORE DISCOUNT (items + oven + shop supplies)
- var subtotalBeforeDiscount = itemsAndOvenSubtotal + shopSuppliesAmount;
+ // 7. SUBTOTAL BEFORE DISCOUNT (items + oven + facility overhead + shop supplies)
+ var subtotalBeforeDiscount = itemsAndOvenSubtotal + facilityOverheadCost + shopSuppliesAmount;
- // 7. CUSTOMER PRICING TIER DISCOUNT (if applicable)
+ // 8. CUSTOMER PRICING TIER DISCOUNT (if applicable)
var pricingTierDiscountPercent = 0m;
var pricingTierDiscountAmount = 0m;
@@ -782,6 +797,8 @@ public class PricingCalculationService : IPricingCalculationService
OvenBatchCost = Math.Round(ovenBatchCost, 2),
OvenBatches = effectiveBatches,
OvenCycleMinutes = effectiveCycleMinutes,
+ FacilityOverheadCost = Math.Round(facilityOverheadCost, 2),
+ FacilityOverheadRatePerHour = Math.Round(facilityOverheadRatePerHour, 4),
ShopSuppliesAmount = Math.Round(shopSuppliesAmount, 2),
ShopSuppliesPercent = Math.Round(shopSuppliesPercent, 2),
OverheadCosts = Math.Round(overheadCosts, 2),
diff --git a/src/PowderCoating.Core/Entities/CompanyOperatingCosts.cs b/src/PowderCoating.Core/Entities/CompanyOperatingCosts.cs
index 9179386..c9e1e5c 100644
--- a/src/PowderCoating.Core/Entities/CompanyOperatingCosts.cs
+++ b/src/PowderCoating.Core/Entities/CompanyOperatingCosts.cs
@@ -127,5 +127,24 @@ namespace PowderCoating.Core.Entities
///
[Range(0, 5000)]
public decimal? CoatingRateSqFtPerHourOverride { get; set; }
+
+ // ── Facility Overhead ─────────────────────────────────────────────────────
+ // Monthly fixed costs divided by billable hours to derive a per-hour overhead
+ // rate, which is then applied to each quote based on estimated labor time.
+
+ /// Monthly shop lease / rent payment.
+ [Range(0, 1000000)]
+ public decimal MonthlyRent { get; set; } = 0m;
+
+ /// Monthly utilities combined (electricity, gas, water, internet).
+ [Range(0, 1000000)]
+ public decimal MonthlyUtilities { get; set; } = 0m;
+
+ ///
+ /// Estimated billable shop hours per month used to amortise facility overhead.
+ /// Defaults to 160 (4 weeks × 40 hrs). Must be at least 1 to avoid division by zero.
+ ///
+ [Range(1, 10000)]
+ public int MonthlyBillableHours { get; set; } = 160;
}
}
diff --git a/src/PowderCoating.Infrastructure/Migrations/20260424232825_AddFacilityOverheadFields.Designer.cs b/src/PowderCoating.Infrastructure/Migrations/20260424232825_AddFacilityOverheadFields.Designer.cs
new file mode 100644
index 0000000..c590aeb
--- /dev/null
+++ b/src/PowderCoating.Infrastructure/Migrations/20260424232825_AddFacilityOverheadFields.Designer.cs
@@ -0,0 +1,9191 @@
+//
+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("20260424232825_AddFacilityOverheadFields")]
+ partial class AddFacilityOverheadFields
+ {
+ ///
+ 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.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("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("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("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("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("IsSystemDefined")
+ .HasColumnType("bit");
+
+ b.Property("RequiresJobLink")
+ .HasColumnType("bit");
+
+ b.Property("TypeCode")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("AppointmentTypeLookups");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.AuditLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Action")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("CompanyName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("EntityDescription")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("EntityId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("EntityType")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("IpAddress")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("NewValues")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("OldValues")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Timestamp")
+ .HasColumnType("datetime2");
+
+ b.Property("UserId")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CompanyId", "Timestamp");
+
+ b.HasIndex("EntityType", "EntityId");
+
+ b.ToTable("AuditLogs");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.BannedIp", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BannedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("BannedByUserId")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("IpAddress")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("Reason")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("BannedIps");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.Bill", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("APAccountId")
+ .HasColumnType("int");
+
+ b.Property("AmountPaid")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("BillDate")
+ .HasColumnType("datetime2");
+
+ b.Property("BillNumber")
+ .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