Files
PowderCoatingLogix/src/PowderCoating.Web/Views/SetupWizard/Step4.cshtml
T
spouliot a0bdd2b5b4 Sweep all .cshtml files for encoding corruption; add pre-commit guard
Replace all corruption variants with HTML entities across 226 view files:
- 3-char UTF-8-as-Win1252 sequences (ae-corruption)
- Standalone smart/curly quotes that break C# Razor expressions
- Partially re-corrupted variants where the 3rd byte was normalised to ASCII

tools/Fix-Encoding.ps1: re-runnable sweep; uses [char] code points so the
script itself never contains a literal non-ASCII character; supports -DryRun

.githooks/pre-commit: blocks commits containing the ae-corruption byte
signature (xc3xa2xe2x82xac); git core.hooksPath = .githooks so the
hook is repo-committed and active for all future work on this machine.

Build clean; 225 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 21:37:10 -04:00

358 lines
21 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@using PowderCoating.Application.DTOs.Wizard
@model WizardOvensStepDto
@{
ViewData["Title"] = "Setup Wizard &mdash; Shop Equipment";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 4;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-tools me-2"></i>Shop Equipment</h2>
<p class="text-secondary">Register your ovens and blast setups. Ovens power the Oven Scheduler and quoting engine &mdash; at least one is required. Blast setups define each rig's throughput rate so the AI quote engine can estimate sandblasting time accurately.</p>
</div>
<form asp-action="PostStep4" method="post" onsubmit="return validateStep4()">
@Html.AntiForgeryToken()
<script type="application/json" id="ovensSeedJson">@Html.Raw(Model.OvensJson ?? "[]")</script>
<script type="application/json" id="blastSetupsSeedJson">@Html.Raw(Model.BlastSetupsJson ?? "[]")</script>
<input type="hidden" name="OvensJson" id="ovensJson" value="[]" />
<input type="hidden" name="BlastSetupsJson" id="blastSetupsJson" value="[]" />
<!-- ── Ovens ─────────────────────────────────────────────────────── -->
<div class="wizard-card">
<h5 class="wizard-card-title"><i class="bi bi-fire me-2"></i>Shop Ovens</h5>
<p class="text-secondary small mb-3">
Each oven appears in the Oven Scheduler's capacity planner and the oven selector on quotes.
Enter dimensions to get a suggested max load, and use the cycle time guide to set a realistic batch time.
</p>
<div id="ovensList"></div>
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addOven()">
<i class="bi bi-plus-circle me-1"></i>Add Oven
</button>
<div id="ovensValidationMsg" class="alert alert-danger alert-permanent d-flex gap-2 mt-3 mb-0 d-none" role="alert">
<i class="bi bi-exclamation-triangle flex-shrink-0 mt-1"></i>
<div class="small">Please add at least one oven before continuing. You can add more ovens later from <strong>Company Settings &rarr; Shop Ovens</strong>.</div>
</div>
</div>
<!-- ── Blast Setups ──────────────────────────────────────────────── -->
<div class="wizard-card">
<h5 class="wizard-card-title"><i class="bi bi-fan me-2"></i>Blast Setups</h5>
<p class="text-secondary small mb-3">
Add each blast rig in your shop (cabinets, pressure pots, blast rooms). The AI quoting engine uses CFM,
nozzle size, and substrate to derive a sqft/hr throughput rate. Mark one as the default &mdash; it will be
pre-selected when quoting. You can also enter a measured rate override if you prefer to use real shop data.
Blast setups are optional; skip this section if you don't do in-house blasting.
</p>
<div id="blastsList"></div>
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addBlast()">
<i class="bi bi-plus-circle me-1"></i>Add Blast Setup
</button>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@section Scripts {
<script>
// ── Shared helpers ────────────────────────────────────────────────────────
function escHtml(str) {
return (str || '').toString()
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ═══════════════════════════════════════════════════════════════════════════
// OVENS
// ═══════════════════════════════════════════════════════════════════════════
var ovens = JSON.parse(document.getElementById('ovensSeedJson').textContent || '[]');
function serializeOvens() {
document.getElementById('ovensJson').value = JSON.stringify(
ovens.map(function(o) {
return { id: o.id || 0, label: o.label, costPerHour: o.costPerHour,
maxLoadSqFt: o.maxLoadSqFt, defaultCycleMinutes: o.defaultCycleMinutes };
})
);
}
function renderOvens() {
var container = document.getElementById('ovensList');
if (ovens.length === 0) {
container.innerHTML = '<p class="text-secondary small py-2">No ovens added yet. Click <strong>Add Oven</strong> to get started.</p>';
} else {
container.innerHTML = ovens.map(function (o, idx) {
return `<div class="wz-item-row mb-2">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Oven Name <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" value="${escHtml(o.label)}"
onchange="updateOven(${idx},'label',this.value)"
placeholder="e.g. Main Oven, Oven #2" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Cost/hr ($) <i class="bi bi-info-circle text-secondary" title="Overrides the default oven rate from Step 3 when this oven is selected on a quote."></i></label>
<input class="form-control form-control-sm" type="number" min="0" step="0.01"
value="${o.costPerHour || 0}"
onchange="updateOvenNum(${idx},'costPerHour',this.value)" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Max Load (sq ft)</label>
<input id="maxLoad_${idx}" class="form-control form-control-sm" type="number" min="0" step="0.1"
value="${o.maxLoadSqFt || ''}"
onchange="updateOvenNum(${idx},'maxLoadSqFt',this.value)"
placeholder="e.g. 80" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Cycle Time (min)</label>
<input id="cycle_${idx}" class="form-control form-control-sm" type="number" min="1" step="1"
value="${o.defaultCycleMinutes || ''}"
onchange="updateOvenNum(${idx},'defaultCycleMinutes',this.value)"
placeholder="e.g. 50" />
</div>
<div class="col-md-2 text-end">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeOven(${idx})" title="Remove">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div class="row g-2 mt-1 align-items-end bg-body-tertiary rounded px-2 py-2">
<div class="col-12 mb-1">
<span class="small fw-semibold text-secondary">
<i class="bi bi-rulers me-1"></i>Dimension Calculator
<span class="fw-normal">(optional &mdash; enter interior oven dimensions)</span>
</span>
</div>
<div class="col-md-2">
<label class="form-label small mb-1">Width (ft)</label>
<input class="form-control form-control-sm" type="number" min="0" step="0.1"
id="dimW_${idx}" value="${o._dimW || ''}" oninput="calcDims(${idx})" placeholder="4" />
</div>
<div class="col-md-2">
<label class="form-label small mb-1">Depth (ft)</label>
<input class="form-control form-control-sm" type="number" min="0" step="0.1"
id="dimD_${idx}" value="${o._dimD || ''}" oninput="calcDims(${idx})" placeholder="6" />
</div>
<div class="col-md-2">
<label class="form-label small mb-1">Height (ft)</label>
<input class="form-control form-control-sm" type="number" min="0" step="0.1"
id="dimH_${idx}" value="${o._dimH || ''}" oninput="calcDims(${idx})" placeholder="6" />
</div>
<div class="col-md-3">
<div id="dimSuggestion_${idx}" class="small text-secondary mt-3"></div>
</div>
<div class="col-md-3 small text-secondary">
<div>Typical cycle: 15 min preheat + 20 min cure + 15 min cooldown = <strong>50 min</strong></div>
<button type="button" class="btn btn-link btn-sm p-0 text-primary" onclick="useCycle(${idx})">Use 50 min</button>
</div>
</div>
</div>`;
}).join('');
}
serializeOvens();
}
function addOven() {
ovens.push({ label: '', costPerHour: 0, maxLoadSqFt: null, defaultCycleMinutes: null });
renderOvens();
document.querySelectorAll('#ovensList .wz-item-row:last-child input')[0]?.focus();
}
function removeOven(idx) { ovens.splice(idx, 1); renderOvens(); }
function updateOven(idx, field, value) { ovens[idx][field] = value; serializeOvens(); }
function updateOvenNum(idx, field, value) {
ovens[idx][field] = value === '' ? null : parseFloat(value);
serializeOvens();
}
function calcDims(idx) {
var w = parseFloat(document.getElementById('dimW_' + idx)?.value) || 0;
var d = parseFloat(document.getElementById('dimD_' + idx)?.value) || 0;
var h = parseFloat(document.getElementById('dimH_' + idx)?.value) || 0;
ovens[idx]._dimW = w || null; ovens[idx]._dimD = d || null; ovens[idx]._dimH = h || null;
var el = document.getElementById('dimSuggestion_' + idx);
if (!el) return;
if (w > 0 && h > 0) {
var sqFt = w * h * 0.7;
var volNote = d > 0 ? ` &nbsp;<span class="text-secondary" style="font-size:0.75rem;">(${(w*d*h).toFixed(1)} cu ft)</span>` : '';
el.innerHTML = `Suggested: <strong>${sqFt.toFixed(1)} sq ft</strong>${volNote}<br/>
<span class="text-secondary" style="font-size:0.75rem;">W × H × 70% usable</span><br/>
<button type="button" class="btn btn-link btn-sm p-0 text-primary" onclick="useSqFt(${idx},${sqFt.toFixed(2)})">Use this</button>`;
} else {
el.innerHTML = '<span class="text-secondary" style="font-size:0.75rem;">Enter width &amp; height for a suggestion.</span>';
}
}
function useSqFt(idx, sqFt) {
ovens[idx].maxLoadSqFt = sqFt;
var input = document.getElementById('maxLoad_' + idx);
if (input) input.value = sqFt;
serializeOvens();
}
function useCycle(idx) {
ovens[idx].defaultCycleMinutes = 50;
var input = document.getElementById('cycle_' + idx);
if (input) input.value = 50;
serializeOvens();
}
// ═══════════════════════════════════════════════════════════════════════════
// BLAST SETUPS
// ═══════════════════════════════════════════════════════════════════════════
var blasts = JSON.parse(document.getElementById('blastSetupsSeedJson').textContent || '[]');
function serializeBlasts() {
document.getElementById('blastSetupsJson').value = JSON.stringify(
blasts.map(function(b) {
return { id: b.id || 0, name: b.name, setupType: b.setupType,
compressorCfm: b.compressorCfm, blastNozzleSize: b.blastNozzleSize,
primarySubstrate: b.primarySubstrate,
blastRateSqFtPerHourOverride: b.blastRateSqFtPerHourOverride,
isDefault: b.isDefault };
})
);
}
function renderBlasts() {
var container = document.getElementById('blastsList');
if (blasts.length === 0) {
container.innerHTML = '<p class="text-secondary small py-2">No blast setups added yet. Click <strong>Add Blast Setup</strong> to get started, or skip if you don\'t do in-house blasting.</p>';
} else {
container.innerHTML = blasts.map(function(b, idx) {
return `<div class="wz-item-row mb-2">
<div class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label small fw-semibold mb-1">Setup Name <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" value="${escHtml(b.name)}"
onchange="updateBlast(${idx},'name',this.value)"
placeholder="e.g. Main Cabinet, Blast Room" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Type</label>
<select class="form-select form-select-sm" onchange="updateBlastNum(${idx},'setupType',this.value)">
<option value="0" ${b.setupType==0?'selected':''}>Siphon Cabinet</option>
<option value="1" ${b.setupType==1?'selected':''}>Siphon Pot</option>
<option value="2" ${b.setupType==2?'selected':''}>Pressure Pot</option>
<option value="3" ${b.setupType==3?'selected':''}>Wet Blasting</option>
</select>
</div>
<div class="col-md-1">
<label class="form-label small fw-semibold mb-1">CFM</label>
<input class="form-control form-control-sm" type="number" min="0" max="9999" step="0.5"
value="${b.compressorCfm || ''}"
onchange="updateBlastNum(${idx},'compressorCfm',this.value)"
placeholder="40" />
</div>
<div class="col-md-1">
<label class="form-label small fw-semibold mb-1">Nozzle #</label>
<select class="form-select form-select-sm" onchange="updateBlastNum(${idx},'blastNozzleSize',this.value)">
<option value="2" ${b.blastNozzleSize==2?'selected':''}>#2</option>
<option value="3" ${b.blastNozzleSize==3?'selected':''}>#3</option>
<option value="4" ${b.blastNozzleSize==4?'selected':''}>#4</option>
<option value="5" ${b.blastNozzleSize==5?'selected':''}>#5</option>
<option value="6" ${b.blastNozzleSize==6?'selected':''}>#6</option>
<option value="7" ${b.blastNozzleSize==7?'selected':''}>#7</option>
<option value="8" ${b.blastNozzleSize==8?'selected':''}>#8</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Primary Substrate</label>
<select class="form-select form-select-sm" onchange="updateBlastNum(${idx},'primarySubstrate',this.value)">
<option value="0" ${b.primarySubstrate==0?'selected':''}>Paint / light</option>
<option value="3" ${b.primarySubstrate==3?'selected':''}>Mixed (typical)</option>
<option value="2" ${b.primarySubstrate==2?'selected':''}>Rust &amp; scale</option>
<option value="1" ${b.primarySubstrate==1?'selected':''}>Existing powder</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Rate Override <span class="text-secondary fw-normal">(sqft/hr)</span></label>
<input class="form-control form-control-sm" type="number" min="0" step="0.1"
value="${b.blastRateSqFtPerHourOverride != null ? b.blastRateSqFtPerHourOverride : ''}"
onchange="updateBlastOverride(${idx},this.value)"
placeholder="leave blank" />
</div>
<div class="col-md-1 d-flex flex-column align-items-center gap-1">
<label class="form-label small fw-semibold mb-1">Default</label>
<input type="checkbox" class="form-check-input blast-default-cb" ${b.isDefault?'checked':''}
onchange="setBlastDefault(${idx},this.checked)" title="Pre-selected in AI quotes" />
</div>
<div class="col-auto">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeBlast(${idx})" title="Remove">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>`;
}).join('');
}
serializeBlasts();
}
function addBlast() {
var isFirst = blasts.length === 0;
blasts.push({ name: '', setupType: 2, compressorCfm: '', blastNozzleSize: 5,
primarySubstrate: 3, blastRateSqFtPerHourOverride: null, isDefault: isFirst });
renderBlasts();
document.querySelectorAll('#blastsList .wz-item-row:last-child input')[0]?.focus();
}
function removeBlast(idx) {
blasts.splice(idx, 1);
// If we just removed the default and there are still items, make the first one default
if (blasts.length > 0 && !blasts.some(function(b) { return b.isDefault; })) {
blasts[0].isDefault = true;
}
renderBlasts();
}
function updateBlast(idx, field, value) { blasts[idx][field] = value; serializeBlasts(); }
function updateBlastNum(idx, field, value) {
blasts[idx][field] = value === '' ? null : parseInt(value, 10);
serializeBlasts();
}
function updateBlastOverride(idx, value) {
blasts[idx].blastRateSqFtPerHourOverride = value === '' ? null : parseFloat(value);
serializeBlasts();
}
function setBlastDefault(idx, checked) {
if (checked) {
blasts.forEach(function(b, i) { b.isDefault = i === idx; });
} else {
blasts[idx].isDefault = false;
}
// Re-render to sync all other checkboxes
renderBlasts();
}
// ═══════════════════════════════════════════════════════════════════════════
// VALIDATION + INIT
// ═══════════════════════════════════════════════════════════════════════════
function validateStep4() {
var valid = ovens.filter(function(o) { return o.label && o.label.trim(); }).length > 0;
document.getElementById('ovensValidationMsg').classList.toggle('d-none', valid);
return valid;
}
renderOvens();
renderBlasts();
</script>
}