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>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
ViewData["Title"] = "Add Inventory Item";
|
||||
ViewData["PageIcon"] = "bi-box-seam";
|
||||
ViewData["PageHelpTitle"] = "Add Inventory Item";
|
||||
ViewData["PageHelpContent"] = "Add a new material to inventory — powder coatings, consumables, or other shop supplies. Select a category first to auto-generate a SKU. Use AI Lookup to fill in manufacturer details from a part number. Set Reorder Point and Reorder Quantity so the system can alert you when stock runs low.";
|
||||
ViewData["PageHelpContent"] = "Add a new material to inventory — powder coatings, consumables, or other shop supplies. Select a category first to auto-generate a SKU. Use AI Lookup to fill in manufacturer details from a part number. Set Reorder Point and Reorder Quantity so the system can alert you when stock runs low.";
|
||||
}
|
||||
|
||||
<div class="row justify-content-center">
|
||||
@@ -29,7 +29,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Basic Information"
|
||||
data-bs-content="Name and SKU are required. Category drives how the item is filtered and used in quotes — choosing a Powder Coating category shows the Coating Specifications section. SKU is auto-generated from the category prefix but you can edit it. Description is optional free text for internal notes.">
|
||||
data-bs-content="Name and SKU are required. Category drives how the item is filtered and used in quotes — choosing a Powder Coating category shows the Coating Specifications section. SKU is auto-generated from the category prefix but you can edit it. Description is optional free text for internal notes.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -85,7 +85,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Product Details"
|
||||
data-bs-content="Manufacturer, part number, color name, color code, and finish describe the physical product. Use Lookup to auto-fill these fields — it checks the product catalog first, then falls back to AI. Coverage is how many sq ft one pound coats at 1 mil thickness (typical: 30). Transfer Efficiency is what percentage of the powder actually sticks (typical: 60–70%). Both values are used to calculate Powder Needed on quotes and jobs.">
|
||||
data-bs-content="Manufacturer, part number, color name, color code, and finish describe the physical product. Use Lookup to auto-fill these fields — it checks the product catalog first, then falls back to AI. Coverage is how many sq ft one pound coats at 1 mil thickness (typical: 30). Transfer Efficiency is what percentage of the powder actually sticks (typical: 60–70%). Both values are used to calculate Powder Needed on quotes and jobs.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -129,7 +129,7 @@
|
||||
<div class="col-md-6">
|
||||
<label asp-for="SdsUrl" class="form-label">Safety Data Sheet (SDS)</label>
|
||||
<div class="input-group">
|
||||
<input asp-for="SdsUrl" class="form-control" id="field-sdsurl" placeholder="https://…" />
|
||||
<input asp-for="SdsUrl" class="form-control" id="field-sdsurl" placeholder="https://…" />
|
||||
<a id="field-sdsurl-link" href="@Model.SdsUrl" target="_blank"
|
||||
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.SdsUrl) ? "d-none" : "")" title="Open SDS">
|
||||
<i class="bi bi-file-earmark-pdf"></i>
|
||||
@@ -140,7 +140,7 @@
|
||||
<div class="col-md-6">
|
||||
<label asp-for="TdsUrl" class="form-label">Technical Data Sheet (TDS)</label>
|
||||
<div class="input-group">
|
||||
<input asp-for="TdsUrl" class="form-control" id="field-tdsurl" placeholder="https://…" />
|
||||
<input asp-for="TdsUrl" class="form-control" id="field-tdsurl" placeholder="https://…" />
|
||||
<a id="field-tdsurl-link" href="@Model.TdsUrl" target="_blank"
|
||||
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.TdsUrl) ? "d-none" : "")" title="Open TDS">
|
||||
<i class="bi bi-file-earmark-text"></i>
|
||||
@@ -193,7 +193,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Transfer Efficiency"
|
||||
data-bs-content="The percentage of powder that actually adheres to the part rather than being lost as overspray. Electrostatic spray guns typically achieve 60–70%. A lower efficiency means you need to order more powder per job. The system uses this value in the Powder Needed calculation on quotes.">
|
||||
data-bs-content="The percentage of powder that actually adheres to the part rather than being lost as overspray. Electrostatic spray guns typically achieve 60–70%. A lower efficiency means you need to order more powder per job. The system uses this value in the Powder Needed calculation on quotes.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -213,7 +213,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Coating Specifications"
|
||||
data-bs-content="Cure Temperature and Cure Time come from the manufacturer's tech data sheet — they tell the oven operator the correct bake profile. Requires Clear Coat flags powders that need a clear top coat for durability or finish. Color Families tag this powder for filtering and matching in the quote wizard (e.g., a teal powder would get both Green and Blue).">
|
||||
data-bs-content="Cure Temperature and Cure Time come from the manufacturer's tech data sheet — they tell the oven operator the correct bake profile. Requires Clear Coat flags powders that need a clear top coat for durability or finish. Color Families tag this powder for filtering and matching in the quote wizard (e.g., a teal powder would get both Green and Blue).">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -267,7 +267,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Stock Information"
|
||||
data-bs-content="Quantity on Hand is your current starting stock. Reorder Point is the threshold at which the system shows a Low Stock alert — when quantity drops to this level it's time to reorder. Reorder Quantity is how much to order in one batch. Location is the shelf or bin label so staff can find the material quickly.">
|
||||
data-bs-content="Quantity on Hand is your current starting stock. Reorder Point is the threshold at which the system shows a Low Stock alert — when quantity drops to this level it's time to reorder. Reorder Quantity is how much to order in one batch. Location is the shelf or bin label so staff can find the material quickly.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -293,7 +293,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Reorder Point"
|
||||
data-bs-content="When Quantity on Hand falls at or below this number a Low Stock warning appears on the item and in the Inventory summary. Set it high enough to cover your lead time — for example if delivery takes a week and you use 2 lb/day, set the reorder point to at least 14 lbs.">
|
||||
data-bs-content="When Quantity on Hand falls at or below this number a Low Stock warning appears on the item and in the Inventory summary. Set it high enough to cover your lead time — for example if delivery takes a week and you use 2 lb/day, set the reorder point to at least 14 lbs.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -306,7 +306,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Reorder Quantity"
|
||||
data-bs-content="The standard quantity to order when restocking — typically a full case or pallet quantity from your supplier. This is informational and appears as a suggested order amount when the item is flagged as low stock.">
|
||||
data-bs-content="The standard quantity to order when restocking — typically a full case or pallet quantity from your supplier. This is informational and appears as a suggested order amount when the item is flagged as low stock.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -325,7 +325,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Pricing & Vendor"
|
||||
data-bs-content="Unit Cost is what you pay per unit (lb, each, etc.) — this is used to calculate total stock value and feeds into job cost calculations. Primary Vendor links to your supplier record for quick reference and purchase ordering.">
|
||||
data-bs-content="Unit Cost is what you pay per unit (lb, each, etc.) — this is used to calculate total stock value and feeds into job cost calculations. Primary Vendor links to your supplier record for quick reference and purchase ordering.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -343,7 +343,7 @@
|
||||
<select asp-for="PrimaryVendorId" class="form-select" id="field-vendor" asp-items="@ViewBag.Vendors"
|
||||
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
||||
<option value="">Select vendor</option>
|
||||
<option value="__new__">+ Add New Vendor…</option>
|
||||
<option value="__new__">+ Add New Vendor…</option>
|
||||
</select>
|
||||
<span asp-validation-for="PrimaryVendorId" class="text-danger"></span>
|
||||
</div>
|
||||
@@ -359,7 +359,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Financial Accounts"
|
||||
data-bs-content="Inventory Account is the asset account where the value of this stock sits on the balance sheet (e.g., 1200 Inventory — Powder). COGS Account is debited when this material is consumed on a job (e.g., 5000 Cost of Goods Sold). Leave blank to use the company defaults set in your Chart of Accounts.">
|
||||
data-bs-content="Inventory Account is the asset account where the value of this stock sits on the balance sheet (e.g., 1200 Inventory — Powder). COGS Account is debited when this material is consumed on a job (e.g., 5000 Cost of Goods Sold). Leave blank to use the company defaults set in your Chart of Accounts.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -370,7 +370,7 @@
|
||||
<select asp-for="InventoryAccountId" class="form-select" asp-items="ViewBag.InventoryAccounts"
|
||||
data-quick-add-url="/Accounts/Create?preSubType=4" data-quick-add-title="Add Inventory Account">
|
||||
<option value="">(Default inventory account)</option>
|
||||
<option value="__new__">+ Add New Account…</option>
|
||||
<option value="__new__">+ Add New Account…</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">Asset account where inventory value is tracked (e.g., 1200 Inventory - Powder).</small>
|
||||
</div>
|
||||
@@ -379,7 +379,7 @@
|
||||
<select asp-for="CogsAccountId" class="form-select" asp-items="ViewBag.CogsAccounts"
|
||||
data-quick-add-url="/Accounts/Create?preSubType=40" data-quick-add-title="Add COGS Account">
|
||||
<option value="">(Default COGS account)</option>
|
||||
<option value="__new__">+ Add New Account…</option>
|
||||
<option value="__new__">+ Add New Account…</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">Expense account debited when this material is consumed on a job.</small>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
ViewData["Title"] = $"{Model.Name}";
|
||||
ViewData["PageIcon"] = "bi-box-seam";
|
||||
ViewData["PageHelpTitle"] = "Inventory Item";
|
||||
ViewData["PageHelpContent"] = "Full detail for this inventory item. Stock Information shows current quantity and reorder thresholds — a Low Stock banner appears when quantity is at or below the Reorder Point. Pricing shows Unit Cost (what you paid), Average Cost (weighted average across purchases), and Total Stock Value. Use the Actions panel to edit, view jobs using this powder, or delete the item.";
|
||||
ViewData["PageHelpContent"] = "Full detail for this inventory item. Stock Information shows current quantity and reorder thresholds — a Low Stock banner appears when quantity is at or below the Reorder Point. Pricing shows Unit Cost (what you paid), Average Cost (weighted average across purchases), and Total Stock Value. Use the Actions panel to edit, view jobs using this powder, or delete the item.";
|
||||
}
|
||||
|
||||
@section Styles {
|
||||
@@ -293,14 +293,14 @@
|
||||
{
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Inventory Account</label>
|
||||
<p class="mb-0">@(Model.InventoryAccountName ?? "—")</p>
|
||||
<p class="mb-0">@(Model.InventoryAccountName ?? "—")</p>
|
||||
</div>
|
||||
}
|
||||
@if (Model.CogsAccountId.HasValue)
|
||||
{
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">COGS Account</label>
|
||||
<p class="mb-0">@(Model.CogsAccountName ?? "—")</p>
|
||||
<p class="mb-0">@(Model.CogsAccountName ?? "—")</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -375,7 +375,7 @@
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="text-muted small mb-1">Location</label>
|
||||
<p class="mb-0">@(Model.Location ?? "—")</p>
|
||||
<p class="mb-0">@(Model.Location ?? "—")</p>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="text-muted small mb-1">Reorder Point</label>
|
||||
@@ -526,15 +526,15 @@
|
||||
<div class="mb-3">
|
||||
<label for="adjReason" class="form-label fw-semibold">Reason <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="adjReason" name="reason" required>
|
||||
<option value="">— Select a reason —</option>
|
||||
<option value="">— Select a reason —</option>
|
||||
<optgroup label="Adding Stock">
|
||||
<option value="Received from purchase order">Received from purchase order</option>
|
||||
<option value="Physical count — found extra">Physical count — found extra</option>
|
||||
<option value="Physical count — found extra">Physical count — found extra</option>
|
||||
<option value="Returned unused product">Returned unused product</option>
|
||||
<option value="Transfer in from another location">Transfer in from another location</option>
|
||||
</optgroup>
|
||||
<optgroup label="Removing Stock">
|
||||
<option value="Physical count — shortage found">Physical count — shortage found</option>
|
||||
<option value="Physical count — shortage found">Physical count — shortage found</option>
|
||||
<option value="Damaged / unusable product">Damaged / unusable product</option>
|
||||
<option value="Waste or spillage">Waste or spillage</option>
|
||||
<option value="Transfer out to another location">Transfer out to another location</option>
|
||||
@@ -551,7 +551,7 @@
|
||||
<div class="mb-1">
|
||||
<label for="adjNotes" class="form-label fw-semibold">Notes <span class="text-muted fw-normal">(optional)</span></label>
|
||||
<textarea class="form-control" id="adjNotes" name="notes" rows="2"
|
||||
placeholder="Any additional details about this adjustment…" maxlength="500"></textarea>
|
||||
placeholder="Any additional details about this adjustment…" maxlength="500"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -697,7 +697,7 @@
|
||||
if (!confirm('This will reduce stock below zero. Continue?')) { e.preventDefault(); return; }
|
||||
}
|
||||
document.getElementById('adjSubmitBtn').disabled = true;
|
||||
document.getElementById('adjSubmitBtn').innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving…';
|
||||
document.getElementById('adjSubmitBtn').innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving…';
|
||||
});
|
||||
|
||||
qtyInput.addEventListener('input', function () { qtyInput.classList.remove('is-invalid'); });
|
||||
@@ -815,7 +815,7 @@
|
||||
|
||||
var from = (page - 1) * pageSize + 1;
|
||||
var to = Math.min(page * pageSize, total);
|
||||
rangeEl.textContent = 'Showing ' + from + '–' + to + ' of ' + total + ' photos';
|
||||
rangeEl.textContent = 'Showing ' + from + '–' + to + ' of ' + total + ' photos';
|
||||
|
||||
paginationEl.innerHTML = '';
|
||||
if (page > 1) addPageBtn(paginationEl, '‹', page - 1);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
ViewData["Title"] = "Edit Inventory Item";
|
||||
ViewData["PageIcon"] = "bi-box-seam";
|
||||
ViewData["PageHelpTitle"] = "Edit Inventory Item";
|
||||
ViewData["PageHelpContent"] = "Update any field on this item. Changes to Coverage or Transfer Efficiency will affect the Powder Needed calculation on future quotes and jobs. Changing Unit Cost does not retroactively update historical job costs — it applies going forward. Use AI Lookup to refresh manufacturer details from a part number.";
|
||||
ViewData["PageHelpContent"] = "Update any field on this item. Changes to Coverage or Transfer Efficiency will affect the Powder Needed calculation on future quotes and jobs. Changing Unit Cost does not retroactively update historical job costs — it applies going forward. Use AI Lookup to refresh manufacturer details from a part number.";
|
||||
}
|
||||
|
||||
<div class="row justify-content-center">
|
||||
@@ -30,7 +30,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Basic Information"
|
||||
data-bs-content="Name and SKU are required. Category determines how the item is used in quotes — Powder Coating items show the Coating Specifications section. Inactive items are hidden from pickers but their historical data is preserved.">
|
||||
data-bs-content="Name and SKU are required. Category determines how the item is used in quotes — Powder Coating items show the Coating Specifications section. Inactive items are hidden from pickers but their historical data is preserved.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -87,7 +87,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Product Details"
|
||||
data-bs-content="Manufacturer, part number, color name, and finish describe the physical product. Coverage is how many sq ft one pound coats (typical: 30). Transfer Efficiency is what percentage sticks to the part (typical: 60–70%). Both values affect the Powder Needed calculation on quotes and jobs. Use AI Lookup to auto-fill fields from a part number.">
|
||||
data-bs-content="Manufacturer, part number, color name, and finish describe the physical product. Coverage is how many sq ft one pound coats (typical: 30). Transfer Efficiency is what percentage sticks to the part (typical: 60–70%). Both values affect the Powder Needed calculation on quotes and jobs. Use AI Lookup to auto-fill fields from a part number.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -131,7 +131,7 @@
|
||||
<div class="col-md-6">
|
||||
<label asp-for="SdsUrl" class="form-label">Safety Data Sheet (SDS)</label>
|
||||
<div class="input-group">
|
||||
<input asp-for="SdsUrl" class="form-control" id="field-sdsurl" placeholder="https://…" />
|
||||
<input asp-for="SdsUrl" class="form-control" id="field-sdsurl" placeholder="https://…" />
|
||||
<a id="field-sdsurl-link" href="@Model.SdsUrl" target="_blank"
|
||||
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.SdsUrl) ? "d-none" : "")" title="Open SDS">
|
||||
<i class="bi bi-file-earmark-pdf"></i>
|
||||
@@ -142,7 +142,7 @@
|
||||
<div class="col-md-6">
|
||||
<label asp-for="TdsUrl" class="form-label">Technical Data Sheet (TDS)</label>
|
||||
<div class="input-group">
|
||||
<input asp-for="TdsUrl" class="form-control" id="field-tdsurl" placeholder="https://…" />
|
||||
<input asp-for="TdsUrl" class="form-control" id="field-tdsurl" placeholder="https://…" />
|
||||
<a id="field-tdsurl-link" href="@Model.TdsUrl" target="_blank"
|
||||
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.TdsUrl) ? "d-none" : "")" title="Open TDS">
|
||||
<i class="bi bi-file-earmark-text"></i>
|
||||
@@ -195,7 +195,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Transfer Efficiency"
|
||||
data-bs-content="The percentage of powder that adheres to the part vs. lost as overspray. Electrostatic guns typically achieve 60–70%. A lower value means more powder is needed per job. The system uses this in the Powder Needed calculation on quotes.">
|
||||
data-bs-content="The percentage of powder that adheres to the part vs. lost as overspray. Electrostatic guns typically achieve 60–70%. A lower value means more powder is needed per job. The system uses this in the Powder Needed calculation on quotes.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -215,7 +215,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Coating Specifications"
|
||||
data-bs-content="Cure Temperature and Cure Time come from the manufacturer's tech data sheet and guide the oven operator. Requires Clear Coat flags powders that need a clear top coat for durability or gloss. Color Families tag this powder for filtering — click chips to add or remove families (e.g., teal = Green + Blue).">
|
||||
data-bs-content="Cure Temperature and Cure Time come from the manufacturer's tech data sheet and guide the oven operator. Requires Clear Coat flags powders that need a clear top coat for durability or gloss. Color Families tag this powder for filtering — click chips to add or remove families (e.g., teal = Green + Blue).">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -321,7 +321,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Pricing & Vendor"
|
||||
data-bs-content="Unit Cost is what you pay per unit — used to calculate total stock value and feeds into job cost calculations. Changing this updates the displayed value going forward but does not change historical job costs. Primary Vendor links to your supplier for quick reference.">
|
||||
data-bs-content="Unit Cost is what you pay per unit — used to calculate total stock value and feeds into job cost calculations. Changing this updates the displayed value going forward but does not change historical job costs. Primary Vendor links to your supplier for quick reference.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -339,7 +339,7 @@
|
||||
<select asp-for="PrimaryVendorId" class="form-select" id="field-vendor" asp-items="@ViewBag.Vendors"
|
||||
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
||||
<option value="">Select vendor</option>
|
||||
<option value="__new__">+ Add New Vendor…</option>
|
||||
<option value="__new__">+ Add New Vendor…</option>
|
||||
</select>
|
||||
<span asp-validation-for="PrimaryVendorId" class="text-danger"></span>
|
||||
</div>
|
||||
@@ -360,7 +360,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Financial Accounts"
|
||||
data-bs-content="Inventory Account is the asset account where stock value sits on the balance sheet (e.g., 1200 Inventory — Powder). COGS Account is debited when this material is consumed on a job (e.g., 5000 Cost of Goods Sold). Leave blank to use the company-wide defaults from your Chart of Accounts.">
|
||||
data-bs-content="Inventory Account is the asset account where stock value sits on the balance sheet (e.g., 1200 Inventory — Powder). COGS Account is debited when this material is consumed on a job (e.g., 5000 Cost of Goods Sold). Leave blank to use the company-wide defaults from your Chart of Accounts.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -371,7 +371,7 @@
|
||||
<select asp-for="InventoryAccountId" class="form-select" asp-items="ViewBag.InventoryAccounts"
|
||||
data-quick-add-url="/Accounts/Create?preSubType=4" data-quick-add-title="Add Inventory Account">
|
||||
<option value="">(Default inventory account)</option>
|
||||
<option value="__new__">+ Add New Account…</option>
|
||||
<option value="__new__">+ Add New Account…</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">Asset account where inventory value is tracked (e.g., 1200 Inventory - Powder).</small>
|
||||
</div>
|
||||
@@ -380,7 +380,7 @@
|
||||
<select asp-for="CogsAccountId" class="form-select" asp-items="ViewBag.CogsAccounts"
|
||||
data-quick-add-url="/Accounts/Create?preSubType=40" data-quick-add-title="Add COGS Account">
|
||||
<option value="">(Default COGS account)</option>
|
||||
<option value="__new__">+ Add New Account…</option>
|
||||
<option value="__new__">+ Add New Account…</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">Expense account debited when this material is consumed on a job.</small>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
ViewData["Title"] = "Inventory";
|
||||
ViewData["PageIcon"] = "bi-box-seam";
|
||||
ViewData["PageHelpTitle"] = "Inventory";
|
||||
ViewData["PageHelpContent"] = "Track powder coatings, consumables, and other shop materials. Items show as Low Stock when quantity falls at or below the Reorder Point — the Low Stock count at the top is your reorder alert. Click any row to view full details or edit. Use the search box and category filter to narrow the list. Low Stock filter shows only items needing attention.";
|
||||
ViewData["PageHelpContent"] = "Track powder coatings, consumables, and other shop materials. Items show as Low Stock when quantity falls at or below the Reorder Point — the Low Stock count at the top is your reorder alert. Click any row to view full details or edit. Use the search box and category filter to narrow the list. Low Stock filter shows only items needing attention.";
|
||||
var lowStockCount = (int)(ViewBag.StatsLowStockCount ?? 0);
|
||||
var activeCount = (int)(ViewBag.StatsActiveCount ?? 0);
|
||||
var totalValue = (decimal)(ViewBag.StatsTotalValue ?? 0m);
|
||||
@@ -131,7 +131,7 @@
|
||||
<i class="bi bi-funnel-fill me-2"></i>
|
||||
@if (lowStockOnly)
|
||||
{
|
||||
<span>Showing <strong>@Model.TotalCount</strong> low stock item@(Model.TotalCount == 1 ? "" : "s") — at or below reorder point</span>
|
||||
<span>Showing <strong>@Model.TotalCount</strong> low stock item@(Model.TotalCount == 1 ? "" : "s") — at or below reorder point</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -247,7 +247,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@@ -257,7 +257,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@model PowderCoating.Application.DTOs.Inventory.InventoryItemDto
|
||||
@{
|
||||
ViewData["Title"] = $"Label — {Model.Name}";
|
||||
ViewData["Title"] = $"Label — {Model.Name}";
|
||||
Layout = null; // standalone print page
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
@@ -8,7 +8,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Inventory Label — @Model.Name</title>
|
||||
<title>Inventory Label — @Model.Name</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
@Model.ColorName
|
||||
@if (!string.IsNullOrEmpty(Model.Finish))
|
||||
{
|
||||
<span> — @Model.Finish</span>
|
||||
<span> — @Model.Finish</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
@if (!string.IsNullOrEmpty(Model.SelectedItemName))
|
||||
{
|
||||
<div class="text-muted small">
|
||||
<i class="bi bi-box-seam me-1"></i>@Model.SelectedItemSku — @Model.SelectedItemName
|
||||
<i class="bi bi-box-seam me-1"></i>@Model.SelectedItemSku — @Model.SelectedItemName
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@* ── Filter Bar ─────────────────────────────────────────────── *@
|
||||
@* -- Filter Bar ----------------------------------------------- *@
|
||||
<form method="get" class="filter-bar">
|
||||
<input type="hidden" name="tab" value="@activeTab" />
|
||||
<div class="d-flex flex-wrap gap-2 align-items-end">
|
||||
@@ -53,7 +53,7 @@
|
||||
@foreach (var item in Model.AllItems)
|
||||
{
|
||||
<option value="@item.Id" selected="@(Model.InventoryItemId == item.Id)">
|
||||
@item.SKU — @item.Name
|
||||
@item.SKU — @item.Name
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
@@ -85,7 +85,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@* ── Summary Pills ───────────────────────────────────────────── *@
|
||||
@* -- Summary Pills --------------------------------------------- *@
|
||||
<div class="d-flex flex-wrap gap-3 mb-3">
|
||||
<div class="stat-pill">
|
||||
<div class="stat-val text-success">@Model.TotalPurchased.ToString("N2")</div>
|
||||
@@ -111,7 +111,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── Tabs ─────────────────────────────────────────────────────── *@
|
||||
@* -- Tabs ------------------------------------------------------- *@
|
||||
<ul class="nav nav-tabs mb-3" id="ledgerTabs">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(activeTab == "transactions" ? "active" : "")"
|
||||
@@ -129,7 +129,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@* ── Transactions Tab ─────────────────────────────────────────── *@
|
||||
@* -- Transactions Tab ------------------------------------------- *@
|
||||
<div id="tab-transactions" class="@(activeTab != "usage" ? "" : "d-none")">
|
||||
@if (!Model.Transactions.Any())
|
||||
{
|
||||
@@ -200,7 +200,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td><small class="text-muted">@t.Notes</small></td>
|
||||
@@ -226,7 +226,7 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ── Usage Tab ────────────────────────────────────────────────── *@
|
||||
@* -- Usage Tab -------------------------------------------------- *@
|
||||
<div id="tab-usage" class="@(activeTab == "usage" ? "" : "d-none")">
|
||||
@if (!Model.PowderUsageLogs.Any())
|
||||
{
|
||||
@@ -291,7 +291,7 @@
|
||||
}
|
||||
</td>
|
||||
}
|
||||
<td>@(u.CoatColor ?? "—")</td>
|
||||
<td>@(u.CoatColor ?? "—")</td>
|
||||
<td class="text-end">@u.EstimatedLbs.ToString("N3")</td>
|
||||
<td class="text-end fw-semibold">@u.ActualLbsUsed.ToString("N3")</td>
|
||||
<td class="text-end @(variance > 0 ? "variance-over" : variance < 0 ? "variance-under" : "")">
|
||||
@@ -344,7 +344,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="editUsageLoading" class="text-center py-4">
|
||||
<div class="spinner-border spinner-border-sm me-2"></div>Loading…
|
||||
<div class="spinner-border spinner-border-sm me-2"></div>Loading…
|
||||
</div>
|
||||
<form id="editUsageForm" class="d-none">
|
||||
@Html.AntiForgeryToken()
|
||||
@@ -356,7 +356,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="euJobId" class="form-label fw-semibold">Job <span class="text-muted fw-normal">(optional)</span></label>
|
||||
<select id="euJobId" name="jobId" class="form-select">
|
||||
<option value="">— No job —</option>
|
||||
<option value="">— No job —</option>
|
||||
</select>
|
||||
<div class="form-text">Select the job this powder was used on.</div>
|
||||
</div>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<input type="hidden" name="tab" value="@activeTab" id="filterTabInput" />
|
||||
<label class="form-label mb-0 fw-semibold me-1">Filter by Manufacturer:</label>
|
||||
<select name="manufacturer" class="form-select form-select-sm w-auto" onchange="this.form.submit()">
|
||||
<option value="">— All Manufacturers —</option>
|
||||
<option value="">— All Manufacturers —</option>
|
||||
@foreach (var mfr in manufacturers)
|
||||
{
|
||||
<option value="@mfr" selected="@(selectedMfr == mfr ? "selected" : null)">@mfr</option>
|
||||
@@ -244,9 +244,9 @@
|
||||
<div class="text-muted small">@item.Name</div>
|
||||
}
|
||||
</td>
|
||||
<td>@(item.Manufacturer ?? "—")</td>
|
||||
<td class="text-muted small">@(item.ManufacturerPartNumber ?? "—")</td>
|
||||
<td>@(item.Finish ?? "—")</td>
|
||||
<td>@(item.Manufacturer ?? "—")</td>
|
||||
<td class="text-muted small">@(item.ManufacturerPartNumber ?? "—")</td>
|
||||
<td>@(item.Finish ?? "—")</td>
|
||||
<td>
|
||||
@if (item.QuantityOnHand > 0)
|
||||
{
|
||||
@@ -260,7 +260,7 @@
|
||||
<td class="text-end pe-3">
|
||||
<button class="btn btn-sm btn-outline-success me-1 btn-toggle-panel"
|
||||
data-item-id="@item.Id" data-has-panel="true"
|
||||
title="Mark as received — panel is on wall">
|
||||
title="Mark as received — panel is on wall">
|
||||
<i class="bi bi-check-lg me-1"></i>Got It
|
||||
</button>
|
||||
<a asp-action="Details" asp-route-id="@item.Id"
|
||||
@@ -399,13 +399,13 @@
|
||||
<div class="text-muted small">@item.Name</div>
|
||||
}
|
||||
</td>
|
||||
<td>@(item.Manufacturer ?? "—")</td>
|
||||
<td class="text-muted small">@(item.ManufacturerPartNumber ?? "—")</td>
|
||||
<td>@(item.Finish ?? "—")</td>
|
||||
<td>@(item.Manufacturer ?? "—")</td>
|
||||
<td class="text-muted small">@(item.ManufacturerPartNumber ?? "—")</td>
|
||||
<td>@(item.Finish ?? "—")</td>
|
||||
<td class="text-end pe-3">
|
||||
<button class="btn btn-sm btn-outline-danger me-1 btn-toggle-panel"
|
||||
data-item-id="@item.Id" data-has-panel="false"
|
||||
title="Remove — panel no longer on wall">
|
||||
title="Remove — panel no longer on wall">
|
||||
<i class="bi bi-x-lg me-1"></i>Remove
|
||||
</button>
|
||||
<a asp-action="Details" asp-route-id="@item.Id"
|
||||
@@ -426,7 +426,7 @@
|
||||
|
||||
<!-- Print-only need-to-order output -->
|
||||
<div id="printArea" style="display:none;">
|
||||
<h3 style="font-family:sans-serif;">Sample Panels — Need to Order</h3>
|
||||
<h3 style="font-family:sans-serif;">Sample Panels — Need to Order</h3>
|
||||
@if (!string.IsNullOrWhiteSpace(selectedMfr))
|
||||
{
|
||||
<p style="font-family:sans-serif;font-size:.9rem;color:#666;">Manufacturer: @selectedMfr</p>
|
||||
@@ -461,7 +461,7 @@
|
||||
<td style="border:1px solid #ccc;padding:6px 10px;">@(item.Manufacturer ?? "")</td>
|
||||
<td style="border:1px solid #ccc;padding:6px 10px;">@(item.ManufacturerPartNumber ?? "")</td>
|
||||
<td style="border:1px solid #ccc;padding:6px 10px;">@(item.Finish ?? "")</td>
|
||||
<td style="border:1px solid #ccc;padding:6px 10px;">@(item.QuantityOnHand > 0 ? item.QuantityOnHand.ToString("N2") + " " + item.UnitOfMeasure : "—")</td>
|
||||
<td style="border:1px solid #ccc;padding:6px 10px;">@(item.QuantityOnHand > 0 ? item.QuantityOnHand.ToString("N2") + " " + item.UnitOfMeasure : "—")</td>
|
||||
<td style="border:1px solid #ccc;padding:6px 10px;"> </td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
var otherJobs = ViewBag.OtherJobs as List<ScanJobOption> ?? new();
|
||||
var preselectedJobId = ViewBag.PreselectedJobId as int?;
|
||||
var scanError = ViewBag.ScanError as string;
|
||||
ViewData["Title"] = $"Log Usage — {item?.Name}";
|
||||
ViewData["Title"] = $"Log Usage — {item?.Name}";
|
||||
Layout = null; // mobile-first standalone page
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
@@ -14,7 +14,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>Log Usage — @item?.Name</title>
|
||||
<title>Log Usage — @item?.Name</title>
|
||||
<style>
|
||||
:root {
|
||||
--purple: #6f42c1;
|
||||
@@ -312,7 +312,7 @@
|
||||
|
||||
<div id="listNone" style="display:none">
|
||||
<div class="no-job-opt selected" onclick="selectNoJob(this)">
|
||||
No job — log as general usage
|
||||
No job — log as general usage
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -363,7 +363,7 @@
|
||||
<div class="field">
|
||||
<label for="notesInput">Notes <span style="font-weight:400;color:var(--muted)">(optional)</span></label>
|
||||
<textarea id="notesInput" name="notes" rows="2"
|
||||
placeholder="Any additional details…" maxlength="500"
|
||||
placeholder="Any additional details…" maxlength="500"
|
||||
style="font-size:16px;resize:none"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -505,7 +505,7 @@
|
||||
|
||||
var btn = document.getElementById('submitBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Saving…';
|
||||
btn.textContent = 'Saving…';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -315,7 +315,7 @@
|
||||
const hasInput = manufacturer || colorName || colorCode || partNumber || itemName;
|
||||
if (!hasInput) {
|
||||
showWarning(
|
||||
'Fill in at least one of: Manufacturer, Color Name, Color Code, Part Number, or the item Name field — then try again.',
|
||||
'Fill in at least one of: Manufacturer, Color Name, Color Code, Part Number, or the item Name field — then try again.',
|
||||
'AI Lookup needs more info'
|
||||
);
|
||||
return;
|
||||
@@ -325,7 +325,7 @@
|
||||
const effectiveColorName = colorName || itemName;
|
||||
|
||||
hideBadMatchBtn();
|
||||
showInfo('Searching for product specifications…', 'AI Lookup');
|
||||
showInfo('Searching for product specifications…', 'AI Lookup');
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
@@ -388,7 +388,7 @@
|
||||
if (!aiFilledFields.includes('field-name')) aiFilledFields.push('field-name');
|
||||
}
|
||||
|
||||
// Spec page URL — fill if blank, and update the open-link button
|
||||
// Spec page URL — fill if blank, and update the open-link button
|
||||
if (data.specPageUrl && (forceRefill || !document.getElementById('field-specpageurl')?.value?.trim())) {
|
||||
const urlEl = document.getElementById('field-specpageurl');
|
||||
const linkEl = document.getElementById('field-specpageurl-link');
|
||||
@@ -446,14 +446,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Product image — show preview if returned and not already set
|
||||
// Product image — show preview if returned and not already set
|
||||
if (data.imageUrl && (forceRefill || !document.getElementById('field-imageurl')?.value?.trim())) {
|
||||
setImagePreview(data.imageUrl);
|
||||
filled.push('Product Image');
|
||||
aiFilledImage = true;
|
||||
}
|
||||
|
||||
// SDS / TDS document URLs — fill inputs and show open-link buttons
|
||||
// SDS / TDS document URLs — fill inputs and show open-link buttons
|
||||
const fillDocUrl = (fieldId, linkId, url, label) => {
|
||||
if (!url) return;
|
||||
const el = document.getElementById(fieldId);
|
||||
@@ -493,7 +493,7 @@
|
||||
: '';
|
||||
showStatus('success', msg + reasoning + tipHtml + debugPanel(data));
|
||||
} else {
|
||||
showWarning('No new fields were filled — they may already be populated, or the product wasn\'t found online.', 'AI Lookup');
|
||||
showWarning('No new fields were filled — they may already be populated, or the product wasn\'t found online.', 'AI Lookup');
|
||||
showStatus('warning', 'No new fields to fill.' + tipHtml + debugPanel(data));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold small">Unit Cost <span class="text-muted fw-normal">(optional — updates item cost)</span></label>
|
||||
<label class="form-label fw-semibold small">Unit Cost <span class="text-muted fw-normal">(optional — updates item cost)</span></label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" id="add-stock-cost" class="form-control" min="0" step="0.01" placeholder="Leave blank to keep current">
|
||||
@@ -88,7 +88,7 @@
|
||||
<!-- Processing overlay: shown while the server lookup is running -->
|
||||
<div id="scan-processing" style="display:none;position:absolute;inset:0;z-index:10;background:rgba(0,0,0,0.88);align-items:center;justify-content:center;flex-direction:column;color:#fff;text-align:center;padding:1.5rem;">
|
||||
<div class="spinner-border text-light mb-3" style="width:2.5rem;height:2.5rem;"></div>
|
||||
<div id="scan-processing-msg" class="fw-medium fs-6">Looking up product…</div>
|
||||
<div id="scan-processing-msg" class="fw-medium fs-6">Looking up product…</div>
|
||||
<div class="text-white-50 small mt-1">This may take a few seconds</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user