Add rework pricing type (Fixed vs Per-Item) and inline rework flow on Job Details

Adds a PricingType enum to ReworkRecord (FixedPrice | PerItem), surfaces the
choice in the rework modal on Job Details, and wires the resulting unit/total
price display. Includes migration AddReworkPricingType, updated repository
query for rework history, and help article updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 09:27:34 -04:00
parent 15b070398b
commit f018653c18
13 changed files with 11143 additions and 169 deletions
+34 -5
View File
@@ -475,12 +475,41 @@
actual hours vs. estimated hours for costing and productivity analysis.
</p>
<h3 class="h6 fw-semibold mt-3 mb-2">Rework</h3>
<h3 class="h6 fw-semibold mt-3 mb-2">Rework (also called Redo)</h3>
<p>
If finished parts fail quality inspection or need to be re-coated, create a rework record
from the Job Details page. Rework records track the rework type, the reason (adhesion failure,
color mismatch, damage, etc.), and the resolution. This data helps identify recurring quality
issues over time.
If a finished part fails quality inspection or a customer returns it damaged, open the original
job&rsquo;s Details page and use the <strong>Rework Log</strong> section to record it. Rework
and redo mean the same thing throughout the system.
</p>
<p>Each entry captures the type (internal defect, customer damage, warranty), the reason (adhesion
failure, color mismatch, runs/sags, insufficient coverage, etc.), a defect description, who
discovered the issue, and pricing responsibility.</p>
<h4 class="h6 fw-semibold mt-3 mb-1">Pricing Responsibility</h4>
<ul>
<li><strong>Shop Fault &mdash; no charge:</strong> All copied item prices are set to $0.</li>
<li><strong>Customer responsible &mdash; reduced rate:</strong> Prices are copied from the original job; edit them down after creation.</li>
<li><strong>Customer responsible &mdash; full price:</strong> Prices are copied as-is.</li>
</ul>
<h4 class="h6 fw-semibold mt-3 mb-1">Creating a Rework Job</h4>
<p>
Toggle <strong>Parts are back &mdash; create a Rework Job</strong> at the top of the log form.
Select the items that need to be redone and choose the pricing responsibility. The system will:
</p>
<ul>
<li>Create a new job with a sub-number (e.g., <code>JOB-2605-0001-R1</code>)</li>
<li>Copy the selected items with their coats and prep services</li>
<li>Auto-record intake &mdash; parts are already on hand when rework is logged</li>
<li>Set the job description to the defect type, reason, and pricing so it is visible at the top of the job</li>
</ul>
<h4 class="h6 fw-semibold mt-3 mb-1">Automatic Resolution</h4>
<p>
When the rework job reaches a terminal status (Completed, Delivered, etc.), the linked rework
record on the original job is automatically marked <strong>Resolved</strong> &mdash; no manual
follow-up needed. If the rework job is <strong>Cancelled</strong>, the record is marked
<strong>Written Off</strong> instead.
</p>
</section>
+130 -18
View File
@@ -28,6 +28,20 @@
</div>
</div>
@if (Model.IsReworkJob && Model.OriginalJobId.HasValue)
{
<div class="alert alert-warning alert-permanent d-flex align-items-center gap-3 mb-4">
<i class="bi bi-arrow-repeat fs-5 flex-shrink-0"></i>
<div>
<strong>Rework Job</strong> &mdash; This job was created to redo work from
<a asp-action="Details" asp-route-id="@Model.OriginalJobId" class="alert-link fw-semibold">
@(Model.OriginalJobNumber ?? $"Job #{Model.OriginalJobId}")
</a>.
All costs for this redo are tracked here separately from the original job.
</div>
</div>
}
<!-- Status Banner -->
<div class="alert alert-@Model.StatusColorClass alert-permanent d-flex align-items-center mb-4">
<i class="bi bi-info-circle me-2" style="font-size: 1.5rem;"></i>
@@ -2189,6 +2203,88 @@
<div class="modal-body">
<div id="reworkAddForm">
<div class="row g-3">
<!-- Step 1: Are parts back in the shop? -->
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="rwCreateJobToggle" onchange="rework.toggleCreateJob(this.checked)" />
<label class="form-check-label fw-semibold" for="rwCreateJobToggle">
<i class="bi bi-briefcase me-1"></i>Parts are back &mdash; create a Rework Job in the shop
</label>
<div class="text-muted small">Turn this on if the parts are physically in the shop and need to go back through the workflow.</div>
</div>
</div>
<!-- Item selection: checkboxes when creating a job, single dropdown otherwise -->
<div id="rwCreateJobOptions" style="display:none;" class="col-12">
<div class="border rounded p-3 bg-light">
<div class="mb-3">
<label class="form-label fw-semibold mb-1">Which items need to be redone? <span class="text-danger">*</span></label>
<div class="text-muted small mb-2">Only checked items will be copied to the rework job.</div>
<div id="rwItemCheckboxes">
@if (Model.Items != null)
{
@foreach (var item in Model.Items)
{
<div class="form-check">
<input class="form-check-input rw-item-cb" type="checkbox" value="@item.Id" id="rwItem_@item.Id" />
<label class="form-check-label" for="rwItem_@item.Id">@item.Description</label>
</div>
}
}
</div>
</div>
<div>
<label class="form-label fw-semibold mb-1">Who is responsible? <span class="text-danger">*</span></label>
<div class="row g-2">
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="radio" name="rwPricingType" id="rwPricingShopFault" value="0" />
<label class="form-check-label" for="rwPricingShopFault">
<strong>Shop Fault</strong>
<span class="text-muted small d-block">Our mistake &mdash; rework job priced at $0.</span>
</label>
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="radio" name="rwPricingType" id="rwPricingCustomerReduced" value="1" />
<label class="form-check-label" for="rwPricingCustomerReduced">
<strong>Customer &mdash; Reduced Rate</strong>
<span class="text-muted small d-block">Customer caused it but we&rsquo;re helping out &mdash; prices copied, edit after creation.</span>
</label>
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="radio" name="rwPricingType" id="rwPricingCustomerFull" value="2" />
<label class="form-check-label" for="rwPricingCustomerFull">
<strong>Customer &mdash; Full Price</strong>
<span class="text-muted small d-block">Customer caused it &mdash; original pricing applies.</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="rwSpecificItemRow" class="col-md-6">
<label class="form-label">Specific Item (optional)</label>
<select class="form-select" id="rwJobItem">
<option value="">&ndash; Whole Job &ndash;</option>
@if (Model.Items != null)
{
@foreach (var item in Model.Items)
{
<option value="@item.Id">@item.Description</option>
}
}
</select>
</div>
<hr class="my-0" />
<div class="col-md-6">
<label class="form-label">Type <span class="text-danger">*</span></label>
<select class="form-select" id="rwType">
@@ -2215,19 +2311,6 @@
<label class="form-label">Defect Description <span class="text-danger">*</span></label>
<textarea class="form-control" id="rwDefect" rows="2" placeholder="Describe the defect or issue..."></textarea>
</div>
<div class="col-md-6">
<label class="form-label">Specific Item (optional)</label>
<select class="form-select" id="rwJobItem">
<option value="">&ndash; Whole Job &ndash;</option>
@if (Model.Items != null)
{
@foreach (var item in Model.Items)
{
<option value="@item.Id">@item.Description</option>
}
}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Discovered By</label>
<select class="form-select" id="rwDiscoveredBy">
@@ -2654,9 +2737,20 @@
document.getElementById('rwBillingNotes').value = '';
document.getElementById('rwReportedBy').value = '';
document.getElementById('rwDiscoveredDate').value = new Date().toISOString().split('T')[0];
// Reset rework job creation section
document.getElementById('rwCreateJobToggle').checked = false;
document.getElementById('rwCreateJobOptions').style.display = 'none';
document.querySelectorAll('.rw-item-cb').forEach(cb => cb.checked = false);
document.querySelectorAll('input[name="rwPricingType"]').forEach(r => r.checked = false);
modal.show();
}
function toggleCreateJob(on) {
document.getElementById('rwCreateJobOptions').style.display = on ? '' : 'none';
document.getElementById('rwSpecificItemRow').style.display = on ? 'none' : '';
if (!on) document.getElementById('rwJobItem').value = '';
}
function openEdit(id) {
editId = id;
const r = records.find(x => x.id === id);
@@ -2685,9 +2779,23 @@
// Create
const defect = document.getElementById('rwDefect').value.trim();
if (!defect) { alert('Defect description is required.'); return; }
const createJob = document.getElementById('rwCreateJobToggle').checked;
const selectedItemIds = createJob
? Array.from(document.querySelectorAll('.rw-item-cb:checked')).map(cb => parseInt(cb.value))
: null;
const pricingRadio = document.querySelector('input[name="rwPricingType"]:checked');
if (createJob && (!selectedItemIds || selectedItemIds.length === 0)) {
alert('Select at least one item to include in the rework job.'); return;
}
if (createJob && !pricingRadio) {
alert('Select who is responsible for this rework.'); return;
}
const dto = {
jobId: jid,
jobItemId: document.getElementById('rwJobItem').value || null,
jobItemId: createJob ? null : (document.getElementById('rwJobItem').value || null),
reworkType: parseInt(document.getElementById('rwType').value),
reason: parseInt(document.getElementById('rwReason').value),
defectDescription: defect,
@@ -2696,7 +2804,10 @@
reportedByName: document.getElementById('rwReportedBy').value || null,
estimatedReworkCost: parseFloat(document.getElementById('rwEstCost').value) || 0,
isBillableToCustomer: document.getElementById('rwBillable').checked,
billingNotes: document.getElementById('rwBillingNotes').value || null
billingNotes: document.getElementById('rwBillingNotes').value || null,
createReworkJob: createJob,
reworkJobItemIds: selectedItemIds,
reworkPricingType: pricingRadio ? parseInt(pricingRadio.value) : null
};
const resp = await fetch('/Jobs/AddReworkRecord', {
method: 'POST',
@@ -2741,7 +2852,7 @@
}
load();
return { load, openAdd, openEdit, save, del };
return { load, openAdd, openEdit, save, del, toggleCreateJob };
})();
</script>
@@ -2906,8 +3017,8 @@
function updateTotals(total) {
const fmt = total > 0 ? total.toFixed(2) + ' hrs' : '&mdash;';
document.getElementById('totalHoursDisplay').textContent = fmt;
document.getElementById('timeEntriesTotalHours').textContent = total > 0 ? total.toFixed(2) : '&mdash;';
document.getElementById('totalHoursDisplay').innerHTML = fmt;
document.getElementById('timeEntriesTotalHours').innerHTML = total > 0 ? total.toFixed(2) : '&mdash;';
}
// -- Modal helpers -------------------------------------------------
@@ -3245,3 +3356,4 @@
</div>
</div>
</div>