Remove ShopWorker entity and migrate worker identity to ApplicationUser

Removes the ShopWorker and ShopWorkerRoleCost entities, all related DTOs,
mappings, controllers, views, and import/export paths. Worker identity is
now handled entirely through ApplicationUser with per-user LaborCostPerHour.
ShopWorkerRoleCosts table remains in production pending manual data migration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 20:32:32 -04:00
parent 3b5511a703
commit 1a44133a63
43 changed files with 10989 additions and 1055 deletions
@@ -756,7 +756,6 @@ public class CompanySettingsController : Controller
var costs = company.OperatingCosts;
var ovens = (await _unitOfWork.OvenCosts.FindAsync(o => o.IsActive)).OrderBy(o => o.DisplayOrder).ToList();
var workers = (await _unitOfWork.ShopWorkers.FindAsync(w => w.IsActive)).ToList();
var coatingCategories = (await _unitOfWork.InventoryCategoryLookups.FindAsync(c => c.IsCoating)).ToList();
var sb = new System.Text.StringBuilder();
@@ -783,8 +782,7 @@ public class CompanySettingsController : Controller
ShopCapabilityTier.Large => "high-volume",
_ => "small"
};
sb.AppendLine($"We are a {tierLabel} operation" +
(workers.Count > 0 ? $" with {workers.Count} active shop worker{(workers.Count == 1 ? "" : "s")}." : "."));
sb.AppendLine($"We are a {tierLabel} operation.");
}
// Ovens
@@ -827,32 +825,6 @@ public class CompanySettingsController : Controller
sb.AppendLine($"Powder categories we stock: {string.Join(", ", catNames)}.");
}
// Worker roles
if (workers.Any())
{
var roles = workers
.Select(w => w.Role)
.Distinct()
.Select(r => r switch
{
ShopWorkerRole.Sandblaster => "sandblasting",
ShopWorkerRole.Coater => "powder coating",
ShopWorkerRole.Masker => "masking",
ShopWorkerRole.QualityControl => "quality control",
ShopWorkerRole.OvenOperator => "oven operation",
ShopWorkerRole.Supervisor => "supervision",
ShopWorkerRole.Maintenance => "equipment maintenance",
_ => "general labor"
})
.Distinct()
.ToList();
if (roles.Count > 1)
{
sb.AppendLine();
sb.AppendLine($"Staff specialties on hand: {string.Join(", ", roles)}.");
}
}
// Rates hint
if (costs != null && costs.StandardLaborRate > 0)
{
@@ -2719,79 +2691,6 @@ public class CompanySettingsController : Controller
// ── Role-Based Labor Rates ────────────────────────────────────────────────
/// <summary>
/// Returns the per-role hourly labor rates configured for the current company, keyed by
/// <see cref="PowderCoating.Core.Enums.ShopWorkerRole"/> integer value. An empty list is returned
/// (rather than a 404) when no rates have been configured yet, so the UI can render the rate grid
/// without special-casing an empty state. The global multi-tenant filter on
/// <c>ShopWorkerRoleCosts</c> ensures only this company's rates are returned.
/// </summary>
[HttpGet]
public async Task<IActionResult> GetRoleCosts()
{
var companyId = _tenantContext.GetCurrentCompanyId();
if (companyId == null) return Json(new List<object>());
var rates = await _unitOfWork.ShopWorkerRoleCosts.FindAsync(r => r.CompanyId == companyId.Value);
var result = rates.Select(r => new { role = (int)r.Role, hourlyRate = r.HourlyRate }).ToList();
return Json(result);
}
/// <summary>
/// Upserts the per-role hourly labor rates for the current company. The operation handles three cases
/// per rate in a single pass: (1) rate cleared (≤ 0) — soft-delete the existing record; (2) rate set
/// but no existing record — insert new; (3) rate changed — update existing. This avoids full
/// table replace semantics that could cause audit log noise or trigger unintended EF change-tracking.
/// These rates are used by the pricing calculator when <c>UseRoleBasedLaborRates</c> is enabled in
/// <c>CompanyOperatingCosts</c>.
/// </summary>
[HttpPost]
public async Task<IActionResult> SaveRoleCosts([FromBody] List<SaveRoleCostDto> rates)
{
try
{
var companyId = _tenantContext.GetCurrentCompanyId();
if (companyId == null) return Json(new { success = false, message = "No company found." });
var existing = (await _unitOfWork.ShopWorkerRoleCosts.FindAsync(r => r.CompanyId == companyId.Value)).ToList();
foreach (var dto in rates)
{
var record = existing.FirstOrDefault(r => (int)r.Role == dto.Role);
if (dto.HourlyRate <= 0)
{
// Remove rate if cleared
if (record != null)
await _unitOfWork.ShopWorkerRoleCosts.SoftDeleteAsync(record.Id);
}
else if (record == null)
{
await _unitOfWork.ShopWorkerRoleCosts.AddAsync(new PowderCoating.Core.Entities.ShopWorkerRoleCost
{
CompanyId = companyId.Value,
Role = (PowderCoating.Core.Enums.ShopWorkerRole)dto.Role,
HourlyRate = dto.HourlyRate,
CreatedAt = DateTime.UtcNow
});
}
else
{
record.HourlyRate = dto.HourlyRate;
record.UpdatedAt = DateTime.UtcNow;
await _unitOfWork.ShopWorkerRoleCosts.UpdateAsync(record);
}
}
await _unitOfWork.CompleteAsync();
return Json(new { success = true });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving role costs");
return Json(new { success = false, message = "An error occurred saving role rates." });
}
}
// ─── Stripe Connect ───────────────────────────────────────────────────────
/// <summary>
@@ -3055,7 +2954,6 @@ public class CompanySettingsController : Controller
}
public record SaveTemplateJsonRequest(int Id, string? Subject, string? Body);
public record SaveRoleCostDto(int Role, decimal HourlyRate);
public record SaveOnlinePaymentSettingsDto(
OnlinePaymentSurchargeType SurchargeType,
decimal SurchargeValue,