Reorganize Operating Costs tab into individual section cards

Replaces single large card with six labeled section cards (Rates & Costs,
Facility Overhead, Equipment, Pricing & Profit, Rush Charges, Complexity)
to reduce visual density and improve scannability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 21:03:43 -04:00
parent b2a1b9a0be
commit 91a5dbe30c
@@ -344,28 +344,35 @@
<!-- Operating Costs Tab -->
<div class="tab-pane fade" id="operating-costs" role="tabpanel">
<div class="card mt-3">
<div class="card-body">
<h5 class="card-title">Operating Costs Configuration
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Operating Costs"
data-bs-content="These are the rates the quoting engine uses to price every job automatically. Set them to your real shop costs and the system will produce accurate quotes without manual calculation. &lt;strong&gt;New quotes use the current rates&lt;/strong&gt; — changing a rate here does not retroactively reprice existing quotes.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Settings#pricing-configuration' target='_blank'&gt;Learn more →&lt;/a&gt;">
<i class="bi bi-question-circle"></i>
</a>
</h5>
<p class="text-muted">Configure your operating costs for accurate job quoting calculations.</p>
<form id="operatingCostsForm">
<form id="operatingCostsForm">
<!-- Rates & Costs -->
<h6 class="border-bottom pb-2 mb-3">Rates &amp; Costs
<!-- Header -->
<div class="card mt-3">
<div class="card-body">
<h5 class="card-title mb-1">Operating Costs Configuration
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Rates &amp; Costs"
data-bs-content="&lt;strong&gt;Standard Labor Rate&lt;/strong&gt; is the baseline $/hr for all coating work — sandblasting and masking are multiplied from this. &lt;strong&gt;Powder Coating Cost/sq ft&lt;/strong&gt; is the fallback material rate used when you don't select a specific powder inventory item on a quote item. &lt;strong&gt;Additional Coat Labor&lt;/strong&gt; is the percentage of the base labor cost charged for each coat after the first (e.g. 30% means a 2nd coat adds 30% more labor).">
data-bs-title="Operating Costs"
data-bs-content="These are the rates the quoting engine uses to price every job automatically. Set them to your real shop costs and the system will produce accurate quotes without manual calculation. &lt;strong&gt;New quotes use the current rates&lt;/strong&gt; — changing a rate here does not retroactively reprice existing quotes.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Settings#pricing-configuration' target='_blank'&gt;Learn more →&lt;/a&gt;">
<i class="bi bi-question-circle"></i>
</a>
</h6>
</h5>
<p class="text-muted mb-0">Configure your operating costs for accurate job quoting calculations.</p>
</div>
</div>
<!-- Rates & Costs -->
<div class="card mt-3 border-0 shadow-sm">
<div class="card-header bg-transparent fw-semibold">
<i class="bi bi-currency-dollar text-primary me-1"></i> Rates &amp; Costs
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Rates &amp; Costs"
data-bs-content="&lt;strong&gt;Standard Labor Rate&lt;/strong&gt; is the baseline $/hr for all coating work — sandblasting and masking are multiplied from this. &lt;strong&gt;Powder Coating Cost/sq ft&lt;/strong&gt; is the fallback material rate used when you don't select a specific powder inventory item on a quote item. &lt;strong&gt;Additional Coat Labor&lt;/strong&gt; is the percentage of the base labor cost charged for each coat after the first (e.g. 30% means a 2nd coat adds 30% more labor).">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="mb-3">
@@ -430,16 +437,21 @@
</div>
</div>
</div>
</div>
</div>
<!-- Facility Overhead -->
<h6 class="border-bottom pb-2 mb-3 mt-3">Facility Overhead
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Facility Overhead"
data-bs-content="Enter your monthly shop rent and combined utility costs. The system divides these by your estimated billable hours to derive a per-hour overhead rate, which is then added to every quote proportionally to the estimated job time. This ensures fixed facility costs are recovered across all jobs rather than absorbed into your markup.">
<i class="bi bi-question-circle"></i>
</a>
</h6>
<!-- Facility Overhead -->
<div class="card mt-3 border-0 shadow-sm">
<div class="card-header bg-transparent fw-semibold">
<i class="bi bi-building text-primary me-1"></i> Facility Overhead
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Facility Overhead"
data-bs-content="Enter your monthly shop rent and combined utility costs. The system divides these by your estimated billable hours to derive a per-hour overhead rate, which is then added to every quote proportionally to the estimated job time. This ensures fixed facility costs are recovered across all jobs rather than absorbed into your markup.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="card-body">
<div class="row align-items-start">
<div class="col-md-3">
<div class="mb-3">
@@ -469,7 +481,7 @@
<input type="number" step="1" class="form-control facility-overhead-input" id="monthlyBillableHours" name="MonthlyBillableHours" value="@(Model.OperatingCosts?.MonthlyBillableHours ?? 160)" min="1" max="10000">
<span class="input-group-text">hrs</span>
</div>
<small class="text-muted">Typical: 160 hrs (4 wks × 40 hrs)</small>
<small class="text-muted">Typical: 160 hrs (4 wks &times; 40 hrs)</small>
</div>
</div>
<div class="col-md-3">
@@ -484,16 +496,21 @@
</div>
</div>
</div>
</div>
</div>
<!-- Equipment Operating Costs -->
<h6 class="border-bottom pb-2 mb-3 mt-3">Equipment Operating Costs
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Equipment Operating Costs"
data-bs-content="The hourly cost of running each piece of equipment, including energy and depreciation. These are added to quote items based on the prep services selected. The &lt;strong&gt;Default Oven Rate&lt;/strong&gt; is used on quotes where no named oven is chosen — add individual shop ovens below if you have multiple ovens with different capacities and costs.">
<i class="bi bi-question-circle"></i>
</a>
</h6>
<!-- Equipment Operating Costs -->
<div class="card mt-3 border-0 shadow-sm">
<div class="card-header bg-transparent fw-semibold">
<i class="bi bi-tools text-primary me-1"></i> Equipment Operating Costs
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Equipment Operating Costs"
data-bs-content="The hourly cost of running each piece of equipment, including energy and depreciation. These are added to quote items based on the prep services selected. The &lt;strong&gt;Default Oven Rate&lt;/strong&gt; is used on quotes where no named oven is chosen — add individual shop ovens below if you have multiple ovens with different capacities and costs.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="mb-3">
@@ -527,16 +544,21 @@
</div>
</div>
</div>
</div>
</div>
<!-- Pricing & Overhead -->
<h6 class="border-bottom pb-2 mb-3 mt-4">Pricing &amp; Profit
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Pricing &amp; Profit"
data-bs-content="&lt;strong&gt;Markup mode&lt;/strong&gt; adds a % on top of material costs only (labor and equipment pass through at cost). &lt;strong&gt;Margin mode&lt;/strong&gt; targets a gross margin % of the total selling price — e.g. 30% margin on a $100 cost base gives a $142.86 price. Note: margin % and markup % are not the same number. &lt;strong&gt;Shop Minimum&lt;/strong&gt; sets a floor price for any job.">
<i class="bi bi-question-circle"></i>
</a>
</h6>
<!-- Pricing & Profit -->
<div class="card mt-3 border-0 shadow-sm">
<div class="card-header bg-transparent fw-semibold">
<i class="bi bi-graph-up-arrow text-primary me-1"></i> Pricing &amp; Profit
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Pricing &amp; Profit"
data-bs-content="&lt;strong&gt;Markup mode&lt;/strong&gt; adds a % on top of material costs only (labor and equipment pass through at cost). &lt;strong&gt;Margin mode&lt;/strong&gt; targets a gross margin % of the total selling price — e.g. 30% margin on a $100 cost base gives a $142.86 price. Note: margin % and markup % are not the same number. &lt;strong&gt;Shop Minimum&lt;/strong&gt; sets a floor price for any job.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="card-body">
@{
var currentPricingMode = (int)(Model.OperatingCosts?.PricingMode ?? PowderCoating.Core.Enums.PricingMode.MarkupOnMaterial);
}
@@ -547,14 +569,14 @@
<input class="form-check-input" type="radio" name="pricingModeRadio" id="pricingModeMarkup" value="0"
@(currentPricingMode == 0 ? "checked" : "") onchange="onPricingModeChange()">
<label class="form-check-label" for="pricingModeMarkup">
<strong>Markup</strong> add % to material costs
<strong>Markup</strong> &mdash; add % to material costs
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="pricingModeRadio" id="pricingModeMargin" value="1"
@(currentPricingMode == 1 ? "checked" : "") onchange="onPricingModeChange()">
<label class="form-check-label" for="pricingModeMargin">
<strong>Margin</strong> target gross margin % of selling price
<strong>Margin</strong> &mdash; target gross margin % of selling price
</label>
</div>
</div>
@@ -592,16 +614,21 @@
</div>
</div>
</div>
</div>
</div>
<!-- Rush Charges -->
<h6 class="border-bottom pb-2 mb-3 mt-3">Rush Charges
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Rush Charges"
data-bs-content="When a quote is marked as a &lt;strong&gt;Rush Job&lt;/strong&gt;, this charge is automatically added to the total. Choose &lt;strong&gt;Percentage&lt;/strong&gt; to add a % of the subtotal (e.g. 25% rush surcharge) or &lt;strong&gt;Fixed Amount&lt;/strong&gt; to add a flat fee (e.g. $75 rush fee). The rush charge appears as its own line on the quote.">
<i class="bi bi-question-circle"></i>
</a>
</h6>
<!-- Rush Charges -->
<div class="card mt-3 border-0 shadow-sm">
<div class="card-header bg-transparent fw-semibold">
<i class="bi bi-lightning-charge text-primary me-1"></i> Rush Charges
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Rush Charges"
data-bs-content="When a quote is marked as a &lt;strong&gt;Rush Job&lt;/strong&gt;, this charge is automatically added to the total. Choose &lt;strong&gt;Percentage&lt;/strong&gt; to add a % of the subtotal (e.g. 25% rush surcharge) or &lt;strong&gt;Fixed Amount&lt;/strong&gt; to add a flat fee (e.g. $75 rush fee). The rush charge appears as its own line on the quote.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
@@ -611,7 +638,6 @@
<label class="btn btn-outline-primary" for="rushChargeTypePercentage">
<i class="bi bi-percent"></i> Percentage
</label>
<input type="radio" class="btn-check" name="rushChargeTypeRadio" id="rushChargeTypeFixed" value="FixedAmount" checked="@((Model.OperatingCosts?.RushChargeType) == "FixedAmount")">
<label class="btn btn-outline-primary" for="rushChargeTypeFixed">
<i class="bi bi-currency-dollar"></i> Fixed Amount
@@ -630,7 +656,6 @@
<small class="text-muted">Percentage of subtotal added for rush jobs</small>
</div>
</div>
<div id="rushChargeFixedInput" style="display: @((Model.OperatingCosts?.RushChargeType) == "FixedAmount" ? "block" : "none")">
<div class="mb-3">
<label for="rushChargeFixedAmount" class="form-label">Rush Charge Amount</label>
@@ -643,65 +668,66 @@
</div>
</div>
</div>
<!-- Part Complexity Multipliers -->
<div class="card mb-4 border-0 shadow-sm">
<div class="card-header bg-transparent fw-semibold">
<i class="bi bi-layers text-primary me-1"></i> Part Complexity Multipliers
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Part Complexity Multipliers"
data-bs-content="A percentage added to the price of &lt;strong&gt;calculated items&lt;/strong&gt; based on how intricate the part is. When adding an item in a quote, staff select a complexity level — the system then applies this multiplier to account for the extra time and care needed. &lt;em&gt;Simple&lt;/em&gt; = 0% (flat panels, basic shapes). &lt;em&gt;Extreme&lt;/em&gt; = highly detailed, tight recesses, masking-intensive parts.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="card-body">
<p class="text-muted small mb-3">Percentage added to the calculated item price based on part intricacy. Applied to calculated items only (not catalog, generic, or labor items).</p>
<div class="row g-3">
<div class="col-sm-6 col-md-3">
<label for="complexitySimplePercent" class="form-label">Simple (%)</label>
<div class="input-group">
<input type="number" step="0.1" class="form-control" id="complexitySimplePercent" name="ComplexitySimplePercent" value="@(Model.OperatingCosts?.ComplexitySimplePercent ?? 0)" min="0" max="500">
<span class="input-group-text">%</span>
</div>
<small class="text-muted">No added complexity</small>
</div>
<div class="col-sm-6 col-md-3">
<label for="complexityModeratePercent" class="form-label">Moderate (%)</label>
<div class="input-group">
<input type="number" step="0.1" class="form-control" id="complexityModeratePercent" name="ComplexityModeratePercent" value="@(Model.OperatingCosts?.ComplexityModeratePercent ?? 5)" min="0" max="500">
<span class="input-group-text">%</span>
</div>
<small class="text-muted">Some detail work</small>
</div>
<div class="col-sm-6 col-md-3">
<label for="complexityComplexPercent" class="form-label">Complex (%)</label>
<div class="input-group">
<input type="number" step="0.1" class="form-control" id="complexityComplexPercent" name="ComplexityComplexPercent" value="@(Model.OperatingCosts?.ComplexityComplexPercent ?? 15)" min="0" max="500">
<span class="input-group-text">%</span>
</div>
<small class="text-muted">Intricate parts</small>
</div>
<div class="col-sm-6 col-md-3">
<label for="complexityExtremePercent" class="form-label">Extreme (%)</label>
<div class="input-group">
<input type="number" step="0.1" class="form-control" id="complexityExtremePercent" name="ComplexityExtremePercent" value="@(Model.OperatingCosts?.ComplexityExtremePercent ?? 25)" min="0" max="500">
<span class="input-group-text">%</span>
</div>
<small class="text-muted">Highly detailed/difficult</small>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary" id="btnSaveOperatingCosts">
<i class="bi bi-save"></i> Save Operating Costs
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Part Complexity Multipliers -->
<div class="card mt-3 border-0 shadow-sm">
<div class="card-header bg-transparent fw-semibold">
<i class="bi bi-layers text-primary me-1"></i> Part Complexity Multipliers
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Part Complexity Multipliers"
data-bs-content="A percentage added to the price of &lt;strong&gt;calculated items&lt;/strong&gt; based on how intricate the part is. When adding an item in a quote, staff select a complexity level — the system then applies this multiplier to account for the extra time and care needed. &lt;em&gt;Simple&lt;/em&gt; = 0% (flat panels, basic shapes). &lt;em&gt;Extreme&lt;/em&gt; = highly detailed, tight recesses, masking-intensive parts.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="card-body">
<p class="text-muted small mb-3">Percentage added to the calculated item price based on part intricacy. Applied to calculated items only (not catalog, generic, or labor items).</p>
<div class="row g-3">
<div class="col-sm-6 col-md-3">
<label for="complexitySimplePercent" class="form-label">Simple (%)</label>
<div class="input-group">
<input type="number" step="0.1" class="form-control" id="complexitySimplePercent" name="ComplexitySimplePercent" value="@(Model.OperatingCosts?.ComplexitySimplePercent ?? 0)" min="0" max="500">
<span class="input-group-text">%</span>
</div>
<small class="text-muted">No added complexity</small>
</div>
<div class="col-sm-6 col-md-3">
<label for="complexityModeratePercent" class="form-label">Moderate (%)</label>
<div class="input-group">
<input type="number" step="0.1" class="form-control" id="complexityModeratePercent" name="ComplexityModeratePercent" value="@(Model.OperatingCosts?.ComplexityModeratePercent ?? 5)" min="0" max="500">
<span class="input-group-text">%</span>
</div>
<small class="text-muted">Some detail work</small>
</div>
<div class="col-sm-6 col-md-3">
<label for="complexityComplexPercent" class="form-label">Complex (%)</label>
<div class="input-group">
<input type="number" step="0.1" class="form-control" id="complexityComplexPercent" name="ComplexityComplexPercent" value="@(Model.OperatingCosts?.ComplexityComplexPercent ?? 15)" min="0" max="500">
<span class="input-group-text">%</span>
</div>
<small class="text-muted">Intricate parts</small>
</div>
<div class="col-sm-6 col-md-3">
<label for="complexityExtremePercent" class="form-label">Extreme (%)</label>
<div class="input-group">
<input type="number" step="0.1" class="form-control" id="complexityExtremePercent" name="ComplexityExtremePercent" value="@(Model.OperatingCosts?.ComplexityExtremePercent ?? 25)" min="0" max="500">
<span class="input-group-text">%</span>
</div>
<small class="text-muted">Highly detailed/difficult</small>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-4 mb-2">
<button type="submit" class="btn btn-primary" id="btnSaveOperatingCosts">
<i class="bi bi-save"></i> Save Operating Costs
</button>
</div>
</form>
</div>
<!-- Oven Cost Add/Edit Modal (outside all forms to avoid form interaction issues) -->
<div class="modal fade" id="ovenModal" tabindex="-1" aria-labelledby="ovenModalTitle" aria-hidden="true">