diff --git a/.gitignore b/.gitignore
index 14cdb0b..95bb3c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -129,3 +129,7 @@ DataProtection-Keys/
# Secrets
appsettings.secrets.json
*.pfx
+
+# Local task tracking
+TODO.txt
+TODO.txt.bak
diff --git a/TODO.txt b/TODO.txt
deleted file mode 100644
index 72fe8c3..0000000
--- a/TODO.txt
+++ /dev/null
@@ -1,226 +0,0 @@
-Shop Management App TO DO List
-==============================
--Add feature to prep for events where we can generate coupons or gift certificates in bulk
-
-Duplication refactor memory
-C:/Users/spoul/.codex/memories/powdercoatingapp-refactor-plan-2026-05-07.md.
-
-Current memory
-C:/Users/spoul/.codex/memories/powdercoatingapp-quote-sync-extracted-2026-05-07.md
-
-
-
--Google review request email after a job
--Check my ChatGPT chat about surface area for a few solid ideas for the system
--Fix up approve/decline messages between customer and user on quote approval feature
-
-Done and need testing
-=====================
--Add sorting to all grids
--Add searching to all grids
--Add Workers to the system
--Allow jobs to be assigned to workers
--Add Shop Job Board display to show in the shop
--Added quick edits on a few pages
--Fix job page customer drop down. It's only showing business names and not individuals
--Add country drop down on customer edit and add pages
--Conver customer once quote accepted not complete
--Add Dashboard page
- -Low Inventory Warnings display
- -Overdue jobs
- -Todays Jobs
--new quote button on customer page doesnt pre-select customer
--Add customer job history page
--Profiles can now change from a light theme to a dark theme as well as other appearance changes
--Date format can be customized per profile
--Timezone can now be changed per profile
--Have company logos stored in the database with the other company information
--Add Company Name under Logo in navbar
--Make logo bigger
--Update create quote page to show names of individual customers or company name depending on which type it is
--Validate that the company has entered operating costs before allowing the quote page to be loaded
--Make phone number and contact required on quotes for new prospects
--Move the create quote button to the right side of the screen to be consistent with other pages
--Add setting for tax exempt on customer
--Added tax certificate upload as well
--Add shop minimum to quoting system and company settings
--Add Rush Job Fee (customizable in company settings)
--Add ability to quick change the status on the job listing and record who changed the status.
--Deactivating company should NOT allow any users to login at all.
--Allow superadmins to create company users/managers
--Add a print quote button
--Add a download PDF button for quotes
--When adding users, also create worker records
--Add quick update to all view pages
--Add Mobile layouts
--Fix a few text pieces on the dashboard page that did not invert properly when dark mode was selected
--Add ability to upload job photos
--Allow photo uploads for jobs before and after photos
--Added Log Viewer
--Added Seed Data option for super admins that will assist during testing
--Add an item list with prices for repeat parts and such
--Add manual data seeding that super admins can use to seed a company one at a time if needed
--Add Log Viewer for Super Admins
--Quotes cleaned up quite a bit and calculations and style changed
--Approving a Quote will now auto-create a Job and link back to the quote it came from.
--Job Items now appear on the Job Screen with the line items from the quote
--Job items can be edited
--Add a way to convert a quote into a job
--Add multiple item types to add to a quote
- 1. Pre-Defined item that we can choose from our product list
- 2. Batch items where we enter the square footage manually as well as the quantity
--Add Quickbooks import for customers and price lists (Desktop and Online)
--Custom Order Powder not saving or displaying properly on quuote page
--Added ability for Companies to define their own Job Status, Job Priority, and Quote Status' via Company Settings > Data Lookups
--Add Randomizer Wheel
--Add Quickbooks format export for
- -Customers
- -Product Catalog
- -Invoices
--Quote for Product Catalog Item is only selecting items from Powder Coating, need all items
--Add a Shop Supplies operating cost that will be used on quote calculations
--Fix Quote screen, only Powder showing in item dropdown. Need to get all items in an IsCoating category showing up.
--Update everywhere that uses tax rate to read and use this setting
--Add ability to export a full price list for known items
--Add tracking for all changes and show change history on view page. Possibly in a hidden grid or modal
--Update the inventory screen to not duplicate color name fields and the like
--Add option for metric system
--Add Bulk Upload for
- -Powder
- -Product Catalog
- -Customer Data
--Add an Appointment engine and Calendar. Also show Maintenence tasks that are scheduled on it
--Allow shops to put employee days off on the calendar as well
--Fix and Verify user permissions are honored
--Run a full security check on the application
--Add support for multi stage coatings on an item
--Fix Seed Data routines to track errors better and continue past error imports
--Add ability to complete a job and enter actual time and materials used
--Add export for all data to CSV format
--Check calendar resizing with the browser. It's off a bit
--Add ability to apply discounts
--Remove powder from inventory when completeing a job
--Add color change ability for appointment types
--Add code to honor the rush charge on a quote
--Add options to quote for Sandblasting, Masking, Chemical Strip, Outgas, Phosphate Wash, Degrease
--Add ability to add sq ft to product catalog item for powder estimation
--Add better UX design for validation errors and such
- Option 1: Change "ModelOnly" to "All" (1 line change) - Shows all validation errors at top of form in red alert box
- - User would have seen: "The field Estimated Minutes must be between 0 and 10,000"
- Option 2: Add inline validation (more complex)
- - Show error messages right next to the problematic field
- - Better UX but requires adding validation spans to dynamic fields
-
- Option 3: Toast notifications (requires new library/code)
- - Modern popup notifications for success/error messages
- - Would need to add a toast library (like Toastr) and wire it up
--Add Import/Export for Company Settings
--Allow Super Admin to modify permissions for company admins in case we add any in the future, or if anything gets messed up we can fix it!
--Allow recurring scheduled maintenance
--Let's show scheduled maintenence on the job schedule as well. At the top of the screen
--Make sure maintenence shows on the calendar list view.
--Add viewing quotes on the customer details page so we can see all quotes/jobs for a given customer to make things easier to find.
--Add support for multiple ovens in operating costs
--Display oven selected on quote and job detail pages
--Allow user to choose an oven on a quote, and have it follow through to a job
--Check for any old and outdated code and DB fields!
--Add ability to email a quote
--Add email capabilities
--Add search on super admin companies screen
--Set limits on job photos per app tier
--Check subscription signup page to make sure the selected subscription is actually saved.
--Don't seed the product catalog on a new user
--Check to make sure subscription page has quotes and all fields on it
--Allow customizing of the quote sheets and invoices (If we do them)
--Add feature to allow username changes
--Fix quickbooks imports based on files Colton sent
--Add thicker border around input fields to signify they are text boxes
--Check to make sure emails get sent when a quote is created
--Add buttons to send emails manually if needed
--Modify price calculations to prompt for service times (ie... sandblasting, oven cure times, outgas times etc)
--Add ability to modify items on jobs
--Swap quoting page to use modals to add items to segregate it a bit better.
--Build account ledger/transaction summary view
--Add security for financial pages
--Allow opening balances for accounts
--Create P&L and other reports
--Allow receipet upload on expenses and bills
--Download PDF for invoices throws and error
--Emailing invoice doesn't seem to trigger
--When a customer record has email notifications turned off, disable any email buttons that may send one and alert the user that this customer is set to have notifications turned off.
--When doing anything that sends mail, prompt the user to alert them a message will be sent
--Create a setup wizard for new users that will walk through system setup. Allow re-running later.
- -Check Workflow steps in wizard, might need adjusting
- -Account Summary, use permanent alert for info message at bottom
- -Add steps so that the new user can customize the data lookups and re-order them
--Reorder menu to work better
--Add ability to print a job invoice once completed
--Add ability to email a job invoice
--Integrate invoicing/billing/reports
--Add customer portal to approve quotes from a link for now. We can do a full login later.
--Need a complexity score for quoting parts (Simple, moderate, complex, extreme)
--Add tagging options for quotes and jobs (user driven)
--Can we also add this tag system to quotes and jobs to allow users to tag themselves and we can use that data later as well? We'd have to add a good
- description of WHY the user should add some tags though.
--Inventory forecasting might be worth looking into
--Build some AI powder usage predictions into the system
--AI Production Scheduling - Batching enough parts together to fill the oven automagically
--Update dashboard to show some $$$ fields
--Update Setup Wizard
--Update the Setup Checklist
--Modify system to keep running balances of all accounts
-- Make sure ALL job updates refresh the Shop Display
--Add multiple item types to add to a quote
-AI Agent item where we upload a picture and it will calculate the approximate sq ft and quote from that
--Integration with stripe or square to accept online paymens from our users customers.
--AI Assistant for help
--Allow customer filtering on quotes and jobs
--New job page blanks when validation fails
--Can we keep track of which users have completed the setup wizard?
--Make sure we're tracking logins. I see a user logged on, but the company health page states they have never logged in.
--Allow printing blank work orders (model after the SCP Powder Coating blank work order)
--IDEA: Print powders to use on work order with their QR code so they can be scanned right from there and usage recorded.
--Add ability to save a quoted item to the product catalog either from an AI Photo Quote or from the calculated item
--Add images to product catalog items for easily identification of parts
--Look into possibly having AI scan a product catalog and suggest prices for items.
--Add Oven and Add Blasting Setup don't work in Setup Wizard
--When scanning inventory QR Code, there is no cancel button
--Bug: When scanning Inventory QR Code, if not logged in...it takes you to the dashboard after login, not our inventory scanning screen
--Add SMS capabilities
--Lookup not working 100% correct. If I type columbia as the manufacturer and a color name....it's finding blackmamba from prismatic incorrectly.
--Lookup Modal not showing ALL matches. Maybe make scrollable
--Pickup cure information from TDS Sheet if not found by AI Search
--ON AI Photo Quote page, when the AI info comes back we should scroll the modal window down so it's visible. It's not clear that new info has been added to the modal for all customers
--Inventory Lookup not always finding price for Columbia Coatings
--Logging powder usage and choosing a job doesn't record properly in the activity section of the powder itself
--Need to allow deleting of powder usage entries, or at least editing in case of a goof up
--Still random weird characters on a bunch of pages. Intake button for example on the jobs screen shows: Intake ✓
-
-5/7/2026
--When editing a job/quote item from catalog, pre-select the item chosen please
--Move buttons to right side of job details page
--When completing a job, pull in powder usage already entered
--Fix invoice due date to match terms selected
--Invoice Status should not show on PDF unless PAID
--If we start with a job, shop supplies is not being added to the items
--If you delete an invoice attached to a job, the create invoice button keeps trying to go back to it
--Customer approval page doesn't show all charges (Oven time missing?)
--Time Logging default user to logged in user
--Add Print Invoice button or allow viewing the PDF
--If an invoice is voided, I cant create a new one from a job. Show voided invoice as history, but allow creating a new one.
--If a completed job is changed after an invoice is created, we need to update the invoice. Also need to be able to modify an invoice to add a discount or similar after it's created
--Add multiple email address for commercial customers (Accounting for invoices and contact for quotes)
--Support entering multiple email addresses (comma seperated) in each field
--If no email on file, then prompt for address to send to.
--When choosing a powder NOT in stock, can we incorporate our inventory lookup function to find a powder, link it to the quote, add it to the inventory with a 0lb balance and still put it on the "powder to order" list?
--When choosing a prospect for a quote, we need way to consent and enable SMS for them
-
-Ideas Removed
-=======================
--Add Deactivate Customer button on Customer Detail page
-
-
-Logins:
-rich@r2r.com/Ragz2Richs123!
-
-rich@cannon.com/Cannon123!
\ No newline at end of file
diff --git a/TODO.txt.bak b/TODO.txt.bak
deleted file mode 100644
index cbdb8e6..0000000
--- a/TODO.txt.bak
+++ /dev/null
@@ -1,226 +0,0 @@
-Shop Management App TO DO List
-==============================
--When editing a job/quote item from catalog, pre-select the item chosen please
--Move buttons to right side of job details page
--When completing a job, pull in powder usage already entered
--Fix invoice due date to match terms selected
--Invoice Status should not show on PDF unless PAID
--If we start with a job, shop supplies is not being added to the items
--If you delete an invoice attached to a job, the create invoice button keeps trying to go back to it
--Customer approval page doesn't show all charges (Oven time missing?)
--Time Logging default user to logged in user
--Add Print Invoice button or allow viewing the PDF
--If an invoice is voided, I cant create a new one from a job. Show voided invoice as history, but allow creating a new one.
--If a completed job is changed after an invoice is created, we need to update the invoice. Also need to be able to modify an invoice to add a discount or similar after it's created
--Add multiple email address for commercial customers (Accounting for invoices and contact for quotes)
--Support entering multiple email addresses (comma seperated) in each field
--If no email on file, then prompt for address to send to.
--When choosing a powder NOT in stock, can we incorporate our inventory lookup function to find a powder, link it to the quote, add it to the inventory with a 0lb balance and still put it on the "powder to order" list?
--When choosing a prospect for a quote, we need way to consent and enable SMS for them
-
-
-
-
-Duplication refactor memory
-C:/Users/spoul/.codex/memories/powdercoatingapp-refactor-plan-2026-05-07.md.
-
-Current memory
-C:/Users/spoul/.codex/memories/powdercoatingapp-quote-sync-extracted-2026-05-07.md
-
-
-
--Google review request email after a job
--Check my ChatGPT chat about surface area for a few solid ideas for the system
--Fix up approve/decline messages between customer and user on quote approval feature
-
-Done and need testing
-=====================
--Add sorting to all grids
--Add searching to all grids
--Add Workers to the system
--Allow jobs to be assigned to workers
--Add Shop Job Board display to show in the shop
--Added quick edits on a few pages
--Fix job page customer drop down. It's only showing business names and not individuals
--Add country drop down on customer edit and add pages
--Conver customer once quote accepted not complete
--Add Dashboard page
- -Low Inventory Warnings display
- -Overdue jobs
- -Todays Jobs
--new quote button on customer page doesnt pre-select customer
--Add customer job history page
--Profiles can now change from a light theme to a dark theme as well as other appearance changes
--Date format can be customized per profile
--Timezone can now be changed per profile
--Have company logos stored in the database with the other company information
--Add Company Name under Logo in navbar
--Make logo bigger
--Update create quote page to show names of individual customers or company name depending on which type it is
--Validate that the company has entered operating costs before allowing the quote page to be loaded
--Make phone number and contact required on quotes for new prospects
--Move the create quote button to the right side of the screen to be consistent with other pages
--Add setting for tax exempt on customer
--Added tax certificate upload as well
--Add shop minimum to quoting system and company settings
--Add Rush Job Fee (customizable in company settings)
--Add ability to quick change the status on the job listing and record who changed the status.
--Deactivating company should NOT allow any users to login at all.
--Allow superadmins to create company users/managers
--Add a print quote button
--Add a download PDF button for quotes
--When adding users, also create worker records
--Add quick update to all view pages
--Add Mobile layouts
--Fix a few text pieces on the dashboard page that did not invert properly when dark mode was selected
--Add ability to upload job photos
--Allow photo uploads for jobs before and after photos
--Added Log Viewer
--Added Seed Data option for super admins that will assist during testing
--Add an item list with prices for repeat parts and such
--Add manual data seeding that super admins can use to seed a company one at a time if needed
--Add Log Viewer for Super Admins
--Quotes cleaned up quite a bit and calculations and style changed
--Approving a Quote will now auto-create a Job and link back to the quote it came from.
--Job Items now appear on the Job Screen with the line items from the quote
--Job items can be edited
--Add a way to convert a quote into a job
--Add multiple item types to add to a quote
- 1. Pre-Defined item that we can choose from our product list
- 2. Batch items where we enter the square footage manually as well as the quantity
--Add Quickbooks import for customers and price lists (Desktop and Online)
--Custom Order Powder not saving or displaying properly on quuote page
--Added ability for Companies to define their own Job Status, Job Priority, and Quote Status' via Company Settings > Data Lookups
--Add Randomizer Wheel
--Add Quickbooks format export for
- -Customers
- -Product Catalog
- -Invoices
--Quote for Product Catalog Item is only selecting items from Powder Coating, need all items
--Add a Shop Supplies operating cost that will be used on quote calculations
--Fix Quote screen, only Powder showing in item dropdown. Need to get all items in an IsCoating category showing up.
--Update everywhere that uses tax rate to read and use this setting
--Add ability to export a full price list for known items
--Add tracking for all changes and show change history on view page. Possibly in a hidden grid or modal
--Update the inventory screen to not duplicate color name fields and the like
--Add option for metric system
--Add Bulk Upload for
- -Powder
- -Product Catalog
- -Customer Data
--Add an Appointment engine and Calendar. Also show Maintenence tasks that are scheduled on it
--Allow shops to put employee days off on the calendar as well
--Fix and Verify user permissions are honored
--Run a full security check on the application
--Add support for multi stage coatings on an item
--Fix Seed Data routines to track errors better and continue past error imports
--Add ability to complete a job and enter actual time and materials used
--Add export for all data to CSV format
--Check calendar resizing with the browser. It's off a bit
--Add ability to apply discounts
--Remove powder from inventory when completeing a job
--Add color change ability for appointment types
--Add code to honor the rush charge on a quote
--Add options to quote for Sandblasting, Masking, Chemical Strip, Outgas, Phosphate Wash, Degrease
--Add ability to add sq ft to product catalog item for powder estimation
--Add better UX design for validation errors and such
- Option 1: Change "ModelOnly" to "All" (1 line change) - Shows all validation errors at top of form in red alert box
- - User would have seen: "The field Estimated Minutes must be between 0 and 10,000"
- Option 2: Add inline validation (more complex)
- - Show error messages right next to the problematic field
- - Better UX but requires adding validation spans to dynamic fields
-
- Option 3: Toast notifications (requires new library/code)
- - Modern popup notifications for success/error messages
- - Would need to add a toast library (like Toastr) and wire it up
--Add Import/Export for Company Settings
--Allow Super Admin to modify permissions for company admins in case we add any in the future, or if anything gets messed up we can fix it!
--Allow recurring scheduled maintenance
--Let's show scheduled maintenence on the job schedule as well. At the top of the screen
--Make sure maintenence shows on the calendar list view.
--Add viewing quotes on the customer details page so we can see all quotes/jobs for a given customer to make things easier to find.
--Add support for multiple ovens in operating costs
--Display oven selected on quote and job detail pages
--Allow user to choose an oven on a quote, and have it follow through to a job
--Check for any old and outdated code and DB fields!
--Add ability to email a quote
--Add email capabilities
--Add search on super admin companies screen
--Set limits on job photos per app tier
--Check subscription signup page to make sure the selected subscription is actually saved.
--Don't seed the product catalog on a new user
--Check to make sure subscription page has quotes and all fields on it
--Allow customizing of the quote sheets and invoices (If we do them)
--Add feature to allow username changes
--Fix quickbooks imports based on files Colton sent
--Add thicker border around input fields to signify they are text boxes
--Check to make sure emails get sent when a quote is created
--Add buttons to send emails manually if needed
--Modify price calculations to prompt for service times (ie... sandblasting, oven cure times, outgas times etc)
--Add ability to modify items on jobs
--Swap quoting page to use modals to add items to segregate it a bit better.
--Build account ledger/transaction summary view
--Add security for financial pages
--Allow opening balances for accounts
--Create P&L and other reports
--Allow receipet upload on expenses and bills
--Download PDF for invoices throws and error
--Emailing invoice doesn't seem to trigger
--When a customer record has email notifications turned off, disable any email buttons that may send one and alert the user that this customer is set to have notifications turned off.
--When doing anything that sends mail, prompt the user to alert them a message will be sent
--Create a setup wizard for new users that will walk through system setup. Allow re-running later.
- -Check Workflow steps in wizard, might need adjusting
- -Account Summary, use permanent alert for info message at bottom
- -Add steps so that the new user can customize the data lookups and re-order them
--Reorder menu to work better
--Add ability to print a job invoice once completed
--Add ability to email a job invoice
--Integrate invoicing/billing/reports
--Add customer portal to approve quotes from a link for now. We can do a full login later.
--Need a complexity score for quoting parts (Simple, moderate, complex, extreme)
--Add tagging options for quotes and jobs (user driven)
--Can we also add this tag system to quotes and jobs to allow users to tag themselves and we can use that data later as well? We'd have to add a good
- description of WHY the user should add some tags though.
--Inventory forecasting might be worth looking into
--Build some AI powder usage predictions into the system
--AI Production Scheduling - Batching enough parts together to fill the oven automagically
--Update dashboard to show some $$$ fields
--Update Setup Wizard
--Update the Setup Checklist
--Modify system to keep running balances of all accounts
-- Make sure ALL job updates refresh the Shop Display
--Add multiple item types to add to a quote
-AI Agent item where we upload a picture and it will calculate the approximate sq ft and quote from that
--Integration with stripe or square to accept online paymens from our users customers.
--AI Assistant for help
--Allow customer filtering on quotes and jobs
--New job page blanks when validation fails
--Can we keep track of which users have completed the setup wizard?
--Make sure we're tracking logins. I see a user logged on, but the company health page states they have never logged in.
--Allow printing blank work orders (model after the SCP Powder Coating blank work order)
--IDEA: Print powders to use on work order with their QR code so they can be scanned right from there and usage recorded.
--Add ability to save a quoted item to the product catalog either from an AI Photo Quote or from the calculated item
--Add images to product catalog items for easily identification of parts
--Look into possibly having AI scan a product catalog and suggest prices for items.
--Add Oven and Add Blasting Setup don't work in Setup Wizard
--When scanning inventory QR Code, there is no cancel button
--Bug: When scanning Inventory QR Code, if not logged in...it takes you to the dashboard after login, not our inventory scanning screen
--Add SMS capabilities
--Lookup not working 100% correct. If I type columbia as the manufacturer and a color name....it's finding blackmamba from prismatic incorrectly.
--Lookup Modal not showing ALL matches. Maybe make scrollable
--Pickup cure information from TDS Sheet if not found by AI Search
--ON AI Photo Quote page, when the AI info comes back we should scroll the modal window down so it's visible. It's not clear that new info has been added to the modal for all customers
--Inventory Lookup not always finding price for Columbia Coatings
--Logging powder usage and choosing a job doesn't record properly in the activity section of the powder itself
--Need to allow deleting of powder usage entries, or at least editing in case of a goof up
--Still random weird characters on a bunch of pages. Intake button for example on the jobs screen shows: Intake ✓
-
-Ideas Removed
-=======================
--Add Deactivate Customer button on Customer Detail page
-
-
-Logins:
-rich@r2r.com/Ragz2Richs123!
-
-rich@cannon.com/Cannon123!
\ No newline at end of file
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