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:
@@ -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 & profit display only — 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 <strong>internal cost rates</strong> (what you pay), not what you bill customers. If a rate is left blank, the <strong>Standard Labor Rate</strong> 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 & 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 — 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 › 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 › 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 & 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 & 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;
|
||||
|
||||
Reference in New Issue
Block a user