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)) {
|
||||
|
||||
Reference in New Issue
Block a user