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
+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>