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
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Company.CompanySettingsDto
@model PowderCoating.Application.DTOs.Company.CompanySettingsDto
@{
ViewData["Title"] = "Company Settings";
ViewData["PageIcon"] = "bi-building";
@@ -375,6 +375,18 @@
<input type="number" step="0.01" class="form-control" id="standardLaborRate" name="StandardLaborRate" value="@(Model.OperatingCosts?.StandardLaborRate ?? 0)" min="0" max="10000" required>
<span class="input-group-text">/hr</span>
</div>
<small class="text-muted">Billing rate used in quotes and pricing</small>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="laborCostPerHour" class="form-label">Shop Labor Cost Rate</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" step="0.01" class="form-control" id="laborCostPerHour" name="LaborCostPerHour" value="@(Model.OperatingCosts?.LaborCostPerHour?.ToString() ?? "")" min="0" max="10000" placeholder="@(((Model.OperatingCosts?.StandardLaborRate ?? 0) * 0.20m).ToString("0.00"))">
<span class="input-group-text">/hr</span>
</div>
<small class="text-muted">Actual wage cost for job costing &amp; profit display only &mdash; never shown to customers. Leave blank to default to 20% of billing rate.</small>
</div>
</div>
<div class="col-md-3">
@@ -516,35 +528,6 @@
</div>
</div>
<!-- Role-Based Labor Rates -->
<h6 class="border-bottom pb-2 mb-3 mt-4">Role-Based Labor Cost Rates
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Role-Based Labor Cost Rates"
data-bs-content="Set an optional cost rate per worker role for job profitability calculations. These are your &lt;strong&gt;internal cost rates&lt;/strong&gt; (what you pay), not what you bill customers. If a rate is left blank, the &lt;strong&gt;Standard Labor Rate&lt;/strong&gt; above is used as the fallback.">
<i class="bi bi-question-circle"></i>
</a>
</h6>
<p class="text-muted small">Used for job costing only — not shown to customers. Leave blank to use the Standard Labor Rate.</p>
<div class="table-responsive mb-3">
<table class="table table-sm align-middle" id="roleCostTable">
<thead class="table-light">
<tr>
<th>Role</th>
<th style="width:180px;">Cost Rate / hr</th>
<th style="width:140px;" class="text-muted small">Fallback if blank</th>
</tr>
</thead>
<tbody id="roleCostBody">
<tr><td colspan="3" class="text-center text-muted py-2"><div class="spinner-border spinner-border-sm me-2"></div>Loading...</td></tr>
</tbody>
</table>
</div>
<button type="button" class="btn btn-sm btn-primary" onclick="saveRoleCosts()">
<i class="bi bi-floppy me-1"></i>Save Labor Rates
</button>
<span id="roleCostSaveStatus" class="ms-2 small"></span>
<!-- Pricing & Overhead -->
<h6 class="border-bottom pb-2 mb-3 mt-4">Pricing &amp; Profit
<a tabindex="0" class="help-icon" role="button"
@@ -2949,76 +2932,6 @@
loadOvenCosts();
});
// Reload role costs whenever the Operating Costs tab is shown
document.getElementById('operating-costs-tab')?.addEventListener('shown.bs.tab', () => {
loadRoleCosts();
});
// If Equipment Profile tab is already active on page load, load immediately
if (document.getElementById('quoting-calibration')?.classList.contains('show')) {
loadOvenCosts();
}
// If Operating Costs tab is already active on page load, load role costs immediately
if (document.getElementById('operating-costs')?.classList.contains('show')) {
loadRoleCosts();
}
// ── Role-Based Labor Cost Rates ────────────────────────────────────────
const ROLE_NAMES = ['General Labor','Sandblaster','Coater','Masker','Quality Control','Oven Operator','Supervisor','Maintenance'];
async function loadRoleCosts() {
const resp = await fetch('/CompanySettings/GetRoleCosts');
const saved = await resp.json(); // [{role, hourlyRate}]
const rateMap = {};
saved.forEach(r => rateMap[r.role] = r.hourlyRate);
const fallbackEl = document.getElementById('standardLaborRate');
const fallback = fallbackEl ? `$${parseFloat(fallbackEl.value || 0).toFixed(2)}/hr` : 'Standard Rate';
const tbody = document.getElementById('roleCostBody');
tbody.innerHTML = ROLE_NAMES.map((name, i) => `
<tr>
<td><span class="badge bg-secondary">${name}</span></td>
<td>
<div class="input-group input-group-sm">
<span class="input-group-text">$</span>
<input type="number" step="0.01" min="0" max="999"
class="form-control role-cost-input"
data-role="${i}"
value="${rateMap[i] > 0 ? rateMap[i] : ''}"
placeholder="(use default)">
</div>
</td>
<td class="text-muted small">${fallback}</td>
</tr>`).join('');
}
async function saveRoleCosts() {
const inputs = document.querySelectorAll('.role-cost-input');
const rates = Array.from(inputs).map(el => ({
role: parseInt(el.dataset.role),
hourlyRate: parseFloat(el.value) || 0
}));
const statusEl = document.getElementById('roleCostSaveStatus');
statusEl.textContent = 'Saving...';
statusEl.className = 'ms-2 small text-muted';
const resp = await fetch('/CompanySettings/SaveRoleCosts', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '' },
body: JSON.stringify(rates)
});
const result = await resp.json();
if (result.success) {
statusEl.textContent = '✓ Saved';
statusEl.className = 'ms-2 small text-success';
setTimeout(() => statusEl.textContent = '', 3000);
} else {
statusEl.textContent = result.message || 'Error saving';
statusEl.className = 'ms-2 small text-danger';
}
}
// ── Quote PDF Template ──────────────────────────────────────────────
function syncColorPicker(hex) {
if (/^#[0-9A-Fa-f]{6}$/.test(hex)) {
@@ -106,6 +106,16 @@
<input asp-for="Position" class="form-control" />
<span asp-validation-for="Position" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="LaborCostPerHour" class="form-label">Labor Cost Rate</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="LaborCostPerHour" type="number" step="0.01" min="0" max="10000" class="form-control" placeholder="Use company default" />
<span class="input-group-text">/hr</span>
</div>
<span asp-validation-for="LaborCostPerHour" class="text-danger"></span>
<small class="text-muted">Used for internal job costing only &mdash; never shown to customers. Overrides the company default when set. Leave blank to use the shop-wide rate.</small>
</div>
<div class="col-md-6">
<label asp-for="HireDate" class="form-label">Hire Date</label>
<input asp-for="HireDate" class="form-control" type="date" />
@@ -189,22 +189,6 @@
<!-- Shop Management -->
<h2 class="h6 fw-semibold mb-2 text-muted text-uppercase" style="letter-spacing:.05em; font-size:.7rem;">Shop Management</h2>
<div class="row g-3 mb-4">
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex align-items-start gap-3">
<div class="rounded-3 bg-info bg-opacity-10 p-2 flex-shrink-0">
<i class="bi bi-person-badge text-info fs-4"></i>
</div>
<div>
<h5 class="card-title mb-1">Shop Workers</h5>
<p class="card-text text-muted small mb-2">Add floor staff, assign roles like Coater or Sandblaster, and link workers to jobs and maintenance tasks.</p>
<a asp-controller="Help" asp-action="ShopWorkers" class="btn btn-sm btn-outline-info">Read more <i class="bi bi-arrow-right ms-1"></i></a>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
@@ -1,226 +0,0 @@
@{
ViewData["Title"] = "Shop Workers";
}
<div class="d-flex align-items-center gap-2 mb-3">
<a asp-controller="Help" asp-action="Index" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i></a>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a asp-controller="Help" asp-action="Index">Help</a></li>
<li class="breadcrumb-item active">Shop Workers</li>
</ol>
</nav>
</div>
<div class="row g-4">
<div class="col-lg-9">
<section id="overview" class="mb-5">
<h2 class="h4 fw-bold border-bottom pb-2 mb-3">
<i class="bi bi-info-circle text-primary me-2"></i>Overview
</h2>
<p>
Shop Workers are the people who do the hands-on work in your facility — sandblasters, coaters,
maskers, oven operators, and supervisors. Adding your workers to the system lets you assign them
to jobs and maintenance tasks, giving you a clear picture of who is working on what at any time.
</p>
<p>
Shop Workers are separate from system user accounts. A worker does not need to log into the
system — they are simply a record that can be assigned to work. If a worker also needs to log
in and update job statuses themselves, an Administrator can create a linked user account for
them with the <em>Shop Floor</em> role.
</p>
<p>
Find Shop Workers under <strong>Operations &rsaquo; Shop Workers</strong> in the sidebar.
</p>
</section>
<section id="adding-a-worker" class="mb-5">
<h2 class="h4 fw-bold border-bottom pb-2 mb-3">
<i class="bi bi-person-plus text-primary me-2"></i>Adding a Worker
</h2>
<p>To add a new shop worker:</p>
<ol class="mb-3">
<li class="mb-2">Go to <strong>Operations &rsaquo; Shop Workers</strong> and click <strong>New Worker</strong>.</li>
<li class="mb-2">
Fill in the worker's details:
<ul class="mt-1">
<li><strong>Name</strong> — the worker's full name as it should appear on job assignments.</li>
<li><strong>Role</strong> — select the role that best describes their primary function (see below).</li>
<li><strong>Phone</strong> — optional, useful for supervisors to have on file.</li>
<li><strong>Email</strong> — optional, used if the worker also has a system login.</li>
<li><strong>Notes</strong> — any relevant information, such as certifications, shift preferences, or specialties.</li>
</ul>
</li>
<li class="mb-2">Ensure <strong>Active</strong> is checked (it is on by default).</li>
<li class="mb-2">Click <strong>Save Worker</strong>.</li>
</ol>
<p>
Once saved, the worker will appear in the assignment dropdowns on the Job Create and Edit forms.
</p>
</section>
<section id="worker-roles" class="mb-5">
<h2 class="h4 fw-bold border-bottom pb-2 mb-3">
<i class="bi bi-tags text-primary me-2"></i>Worker Roles
</h2>
<p>
Each worker is assigned one of the following roles. The role is a label — it helps you pick the
right person for a job but does not restrict what a worker can be assigned to.
</p>
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th style="width:25%">Role</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="badge bg-secondary">General Labor</span></td>
<td>
Versatile workers who assist across multiple areas of the shop — loading and unloading,
racking parts, clean-up, and general support tasks. Not specialized in a single process.
</td>
</tr>
<tr>
<td><span class="badge bg-warning text-dark">Sandblaster</span></td>
<td>
Operates the sandblasting or media-blasting equipment to prepare metal surfaces for
coating. Responsible for achieving the correct surface profile and ensuring all rust,
paint, and contamination is removed.
</td>
</tr>
<tr>
<td><span class="badge bg-primary">Coater</span></td>
<td>
Applies powder coating using an electrostatic spray gun. Responsible for even coverage,
correct mil thickness, and minimizing overspray and waste. Often the most skilled
technical role on the floor.
</td>
</tr>
<tr>
<td><span class="badge bg-info text-dark">Masker</span></td>
<td>
Applies masking tape, plugs, and caps to protect threads, bearing surfaces, and areas
that must not be coated. Attention to detail is critical — missed masking means rework.
</td>
</tr>
<tr>
<td><span class="badge bg-success">Quality Control</span></td>
<td>
Inspects finished parts for adhesion, color consistency, coverage, and surface defects
before the job is marked as complete. May also handle pre-coat inspection after
sandblasting.
</td>
</tr>
<tr>
<td><span class="badge bg-danger">Oven Operator</span></td>
<td>
Loads parts into the curing oven, sets correct temperatures and cure times for the
powder being used, monitors the cure cycle, and unloads parts safely after cooling.
</td>
</tr>
<tr>
<td><span class="badge bg-dark">Supervisor</span></td>
<td>
Oversees day-to-day shop floor operations, assigns tasks to other workers, ensures
jobs are progressing on schedule, and handles escalations. May also handle customer
communication for production updates.
</td>
</tr>
<tr>
<td><span class="badge bg-secondary">Maintenance</span></td>
<td>
Responsible for keeping equipment running — performing scheduled preventive maintenance,
troubleshooting breakdowns, and coordinating with external service technicians when
needed.
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="assigning-to-jobs" class="mb-5">
<h2 class="h4 fw-bold border-bottom pb-2 mb-3">
<i class="bi bi-briefcase text-primary me-2"></i>Assigning Workers to Jobs
</h2>
<p>
Each job can have one worker assigned to it as the primary responsible person. This is the
worker who owns the job from start to finish — typically a coater or supervisor.
</p>
<p>To assign a worker when creating or editing a job:</p>
<ol class="mb-3">
<li class="mb-1">Open the job's Create or Edit form.</li>
<li class="mb-1">Scroll down to the <strong>Assignment</strong> section.</li>
<li class="mb-1">Select a worker from the <strong>Assigned Worker</strong> dropdown. Only active workers are listed.</li>
<li class="mb-1">Save the job.</li>
</ol>
<p>
The assigned worker's name appears on the job list view, on the job detail page, and in any
reports filtered by worker.
</p>
<p>
Workers can also be assigned to <strong>maintenance tasks</strong> on equipment. See the
<a asp-controller="Help" asp-action="Equipment" class="text-decoration-none">Equipment &amp; Maintenance</a>
help page for details.
</p>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
<div>
If a worker you want to assign does not appear in the dropdown, check that their record is
marked as <strong>Active</strong>. Inactive workers are hidden from assignment lists.
</div>
</div>
</section>
<section id="deactivating-a-worker" class="mb-5">
<h2 class="h4 fw-bold border-bottom pb-2 mb-3">
<i class="bi bi-person-dash text-primary me-2"></i>Deactivating a Worker
</h2>
<p>
When a worker leaves the shop or is no longer available for assignment, deactivate their record
rather than deleting it. Deactivating preserves the history of all jobs they were assigned to,
while removing them from the active assignment dropdowns so they cannot be accidentally selected
for new work.
</p>
<p>To deactivate a worker:</p>
<ol class="mb-3">
<li class="mb-1">Open the worker's Details or Edit page.</li>
<li class="mb-1">Uncheck the <strong>Active</strong> checkbox.</li>
<li class="mb-1">Click <strong>Save</strong>.</li>
</ol>
<p>
Alternatively, use the <strong>Delete</strong> button on the Details page to perform a soft
delete, which has the same effect.
</p>
<div class="alert alert-permanent alert-secondary d-flex gap-2 mb-0" role="alert">
<i class="bi bi-info-circle flex-shrink-0 mt-1"></i>
<div>
If a worker currently has open jobs assigned to them, reassign those jobs first before
deactivating the worker — so the jobs remain clearly owned and nothing falls through the cracks.
</div>
</div>
</section>
</div>
<div class="col-lg-3 d-none d-lg-block">
@{ await Html.RenderPartialAsync("_HelpNav"); }
<div class="card border-0 shadow-sm sticky-top" style="top:80px">
<div class="card-header bg-transparent fw-semibold small text-muted text-uppercase" style="letter-spacing:.05em; font-size:.7rem;">On this page</div>
<div class="card-body p-0">
<nav class="nav flex-column">
<a class="nav-link py-1 px-3 small text-body" href="#overview">Overview</a>
<a class="nav-link py-1 px-3 small text-body" href="#adding-a-worker">Adding a Worker</a>
<a class="nav-link py-1 px-3 small text-body" href="#worker-roles">Worker Roles</a>
<a class="nav-link py-1 px-3 small text-body" href="#assigning-to-jobs">Assigning to Jobs</a>
<a class="nav-link py-1 px-3 small text-body" href="#deactivating-a-worker">Deactivating a Worker</a>
</nav>
</div>
</div>
</div>
</div>
@@ -65,11 +65,7 @@
<div class="px-3 pt-2 pb-1">
<span class="text-muted text-uppercase" style="font-size:.65rem; letter-spacing:.07em; font-weight:600;">Shop Management</span>
</div>
<a class="nav-link py-2 px-3 d-flex align-items-center gap-2 @(currentAction == "ShopWorkers" ? "active fw-semibold text-primary" : "text-body")"
asp-controller="Help" asp-action="ShopWorkers">
<i class="bi bi-person-badge"></i> Shop Workers
</a>
<a class="nav-link py-2 px-3 d-flex align-items-center gap-2 @(currentAction == "Equipment" ? "active fw-semibold text-primary" : "text-body")"
<a class="nav-link py-2 px-3 d-flex align-items-center gap-2 @(currentAction == "Equipment" ? "active fw-semibold text-primary" : "text-body")"
asp-controller="Help" asp-action="Equipment">
<i class="bi bi-tools"></i> Equipment &amp; Maintenance
</a>
@@ -1067,8 +1067,7 @@
var hasEquipment = User.HasClaim("Permission", "ManageEquipment") || User.IsInRole("SuperAdmin");
var hasMaintenance = User.HasClaim("Permission", "ManageMaintenance") || User.IsInRole("SuperAdmin");
var hasFinance = _isAdminOrManager || User.HasClaim("Permission", "ManageFinance");
var hasShopWorkers = _isAdminOrManager || User.HasClaim("Permission", "ManageShopWorkers");
var hasReports = _isAdminOrManager || User.HasClaim("Permission", "ViewReports");
var hasReports = _isAdminOrManager || User.HasClaim("Permission", "ViewReports");
var showOperations = hasCustomers || hasQuotes || hasInvoices || hasJobs || hasCalendar;
var showInventorySection = hasInventory || hasVendors;
var showEquipmentSection = hasEquipment || hasMaintenance;