Compare commits

..

6 Commits

Author SHA1 Message Date
spouliot 7020797a25 Merge dev: tax-exempt pricing fixes, job details Unicode cleanup
- Fix tax-exempt customers being charged tax on all job save/recalc paths (7 call sites in JobsController)
- Fix JS falsy-zero bug in quote preview tax calculation (item-wizard.js)
- Fix quote preview not recalculating on customer change (Create.cshtml)
- Add AddQuotePricingSnapshotFields migration (missing from prior session)
- Fix intake button rendering ✓ as literal text (Html.Raw fix)
- Clean up corrupted Unicode box-drawing chars in Job Details view

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:52:39 -04:00
spouliot 3b5511a703 Fix corrupted Unicode characters and intake button rendering in Job Details
- Replace mojibake box-drawing chars (U+2500 encoded as Windows-1252) with
  plain ASCII dashes throughout all comments in Details.cshtml
- Fix intake button showing literal '&#10003;' text: the entity was inside a
  C# string so Razor HTML-encoded the '&'; switched to Html.Raw() so the
  checkmark renders correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:43:36 -04:00
spouliot 8df37ca760 Fix tax-exempt customers charged tax on all job save paths
Jobs used company default TaxPercent for every pricing recalculation
(Create, Edit, UpdateItems, DeleteJobItem) without checking the customer's
IsTaxExempt flag. Added GetEffectiveTaxPercentAsync helper and wired it
into all seven call sites so tax-exempt customers are never billed tax
regardless of which path triggers the recalc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:15:43 -04:00
spouliot 7239f55308 Fix tax-exempt customers always charged tax in quote preview
parseFloat('0') is falsy in JS, so '0 || pageMeta.taxPercent' was
falling through to the company default rate even when the TaxPercent
field was correctly set to 0 for a tax-exempt customer. Use an
explicit field presence check instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:05:07 -04:00
spouliot 09e077897b Fix quote preview not recalculating when tax-exempt customer is selected
When a customer was changed to/from a tax-exempt customer, the hidden
TaxPercent field was correctly updated to 0 but the live pricing preview
was not re-run, so the display showed a stale total with tax applied.
Selecting a tax-exempt customer now immediately triggers a recalc so
the on-screen total matches the amount that will be saved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:58:20 -04:00
spouliot 051c86810e Add missing AddQuotePricingSnapshotFields migration
Seven new decimal columns on Quotes table that were added to the entity
in the pricing audit but the migration was never created (name collision
with a prior attempt in the previous session caused the scaffold to fail).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:48:46 -04:00
7 changed files with 10985 additions and 28 deletions
@@ -0,0 +1,138 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PowderCoating.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddQuotePricingSnapshotFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "FacilityOverheadCost",
table: "Quotes",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "FacilityOverheadRatePerHour",
table: "Quotes",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "PricingTierDiscountAmount",
table: "Quotes",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "PricingTierDiscountPercent",
table: "Quotes",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "QuoteDiscountAmount",
table: "Quotes",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "QuoteDiscountPercent",
table: "Quotes",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "SubtotalAfterDiscount",
table: "Quotes",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 1,
column: "CreatedAt",
value: new DateTime(2026, 5, 15, 19, 43, 40, 586, DateTimeKind.Utc).AddTicks(845));
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 2,
column: "CreatedAt",
value: new DateTime(2026, 5, 15, 19, 43, 40, 586, DateTimeKind.Utc).AddTicks(850));
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 3,
column: "CreatedAt",
value: new DateTime(2026, 5, 15, 19, 43, 40, 586, DateTimeKind.Utc).AddTicks(852));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FacilityOverheadCost",
table: "Quotes");
migrationBuilder.DropColumn(
name: "FacilityOverheadRatePerHour",
table: "Quotes");
migrationBuilder.DropColumn(
name: "PricingTierDiscountAmount",
table: "Quotes");
migrationBuilder.DropColumn(
name: "PricingTierDiscountPercent",
table: "Quotes");
migrationBuilder.DropColumn(
name: "QuoteDiscountAmount",
table: "Quotes");
migrationBuilder.DropColumn(
name: "QuoteDiscountPercent",
table: "Quotes");
migrationBuilder.DropColumn(
name: "SubtotalAfterDiscount",
table: "Quotes");
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 1,
column: "CreatedAt",
value: new DateTime(2026, 5, 15, 16, 29, 32, 589, DateTimeKind.Utc).AddTicks(4618));
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 2,
column: "CreatedAt",
value: new DateTime(2026, 5, 15, 16, 29, 32, 589, DateTimeKind.Utc).AddTicks(4623));
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 3,
column: "CreatedAt",
value: new DateTime(2026, 5, 15, 16, 29, 32, 589, DateTimeKind.Utc).AddTicks(4625));
}
}
}
@@ -6714,7 +6714,7 @@ namespace PowderCoating.Infrastructure.Migrations
{ {
Id = 1, Id = 1,
CompanyId = 0, CompanyId = 0,
CreatedAt = new DateTime(2026, 5, 15, 16, 29, 32, 589, DateTimeKind.Utc).AddTicks(4618), CreatedAt = new DateTime(2026, 5, 15, 19, 43, 40, 586, DateTimeKind.Utc).AddTicks(845),
Description = "Standard pricing for regular customers", Description = "Standard pricing for regular customers",
DiscountPercent = 0m, DiscountPercent = 0m,
IsActive = true, IsActive = true,
@@ -6725,7 +6725,7 @@ namespace PowderCoating.Infrastructure.Migrations
{ {
Id = 2, Id = 2,
CompanyId = 0, CompanyId = 0,
CreatedAt = new DateTime(2026, 5, 15, 16, 29, 32, 589, DateTimeKind.Utc).AddTicks(4623), CreatedAt = new DateTime(2026, 5, 15, 19, 43, 40, 586, DateTimeKind.Utc).AddTicks(850),
Description = "5% discount for preferred customers", Description = "5% discount for preferred customers",
DiscountPercent = 5m, DiscountPercent = 5m,
IsActive = true, IsActive = true,
@@ -6736,7 +6736,7 @@ namespace PowderCoating.Infrastructure.Migrations
{ {
Id = 3, Id = 3,
CompanyId = 0, CompanyId = 0,
CreatedAt = new DateTime(2026, 5, 15, 16, 29, 32, 589, DateTimeKind.Utc).AddTicks(4625), CreatedAt = new DateTime(2026, 5, 15, 19, 43, 40, 586, DateTimeKind.Utc).AddTicks(852),
Description = "10% discount for premium customers", Description = "10% discount for premium customers",
DiscountPercent = 10m, DiscountPercent = 10m,
IsActive = true, IsActive = true,
@@ -6983,6 +6983,12 @@ namespace PowderCoating.Infrastructure.Migrations
b.Property<DateTime?>("ExpirationDate") b.Property<DateTime?>("ExpirationDate")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
b.Property<decimal>("FacilityOverheadCost")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("FacilityOverheadRatePerHour")
.HasColumnType("decimal(18,2)");
b.Property<bool>("HideDiscountFromCustomer") b.Property<bool>("HideDiscountFromCustomer")
.HasColumnType("bit"); .HasColumnType("bit");
@@ -7028,6 +7034,12 @@ namespace PowderCoating.Infrastructure.Migrations
b.Property<string>("PreparedById") b.Property<string>("PreparedById")
.HasColumnType("nvarchar(450)"); .HasColumnType("nvarchar(450)");
b.Property<decimal>("PricingTierDiscountAmount")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("PricingTierDiscountPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("ProfitMargin") b.Property<decimal>("ProfitMargin")
.HasColumnType("decimal(18,2)"); .HasColumnType("decimal(18,2)");
@@ -7067,6 +7079,12 @@ namespace PowderCoating.Infrastructure.Migrations
b.Property<DateTime>("QuoteDate") b.Property<DateTime>("QuoteDate")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
b.Property<decimal>("QuoteDiscountAmount")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("QuoteDiscountPercent")
.HasColumnType("decimal(18,2)");
b.Property<string>("QuoteNumber") b.Property<string>("QuoteNumber")
.IsRequired() .IsRequired()
.HasColumnType("nvarchar(450)"); .HasColumnType("nvarchar(450)");
@@ -7092,6 +7110,9 @@ namespace PowderCoating.Infrastructure.Migrations
b.Property<decimal>("SubTotal") b.Property<decimal>("SubTotal")
.HasColumnType("decimal(18,2)"); .HasColumnType("decimal(18,2)");
b.Property<decimal>("SubtotalAfterDiscount")
.HasColumnType("decimal(18,2)");
b.Property<string>("Tags") b.Property<string>("Tags")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
@@ -422,7 +422,7 @@ public class JobsController : Controller
// Populate Edit Items wizard data (inline modal on Details page) // Populate Edit Items wizard data (inline modal on Details page)
var wizardCosts = await _pricingService.GetOperatingCostsAsync(job.CompanyId); var wizardCosts = await _pricingService.GetOperatingCostsAsync(job.CompanyId);
await PopulateJobItemDropDownsAsync(job.CompanyId, wizardCosts?.OvenOperatingCostPerHour ?? 45m); await PopulateJobItemDropDownsAsync(job.CompanyId, wizardCosts?.OvenOperatingCostPerHour ?? 45m);
ViewBag.WizardTaxPercent = wizardCosts?.TaxPercent ?? 0m; ViewBag.WizardTaxPercent = await GetEffectiveTaxPercentAsync(job.CustomerId, wizardCosts?.TaxPercent ?? 0m);
// Display the pricing snapshot stored when items were last saved. // Display the pricing snapshot stored when items were last saved.
// Never recalculate on load — operating cost changes must not retroactively alter existing jobs. // Never recalculate on load — operating cost changes must not retroactively alter existing jobs.
@@ -1130,7 +1130,7 @@ public class JobsController : Controller
} }
var totals = await _pricingService.CalculateQuoteTotalsAsync( var totals = await _pricingService.CalculateQuoteTotalsAsync(
dto.JobItems, companyId, dto.CustomerId, dto.JobItems, companyId, dto.CustomerId,
createCosts?.TaxPercent ?? 0m, await GetEffectiveTaxPercentAsync(dto.CustomerId, createCosts?.TaxPercent ?? 0m),
dto.DiscountType, dto.DiscountValue, dto.IsRushJob, createOvenRate, job.OvenBatches, job.OvenCycleMinutes); dto.DiscountType, dto.DiscountValue, dto.IsRushJob, createOvenRate, job.OvenBatches, job.OvenCycleMinutes);
job.FinalPrice = totals.Total; job.FinalPrice = totals.Total;
@@ -1598,7 +1598,7 @@ public class JobsController : Controller
} }
var totals = await _pricingService.CalculateQuoteTotalsAsync( var totals = await _pricingService.CalculateQuoteTotalsAsync(
dto.JobItems, companyId, dto.CustomerId, dto.JobItems, companyId, dto.CustomerId,
editCosts?.TaxPercent ?? 0m, await GetEffectiveTaxPercentAsync(dto.CustomerId, editCosts?.TaxPercent ?? 0m),
dto.DiscountType, dto.DiscountValue, dto.IsRushJob, editOvenRate, job.OvenBatches, job.OvenCycleMinutes); dto.DiscountType, dto.DiscountValue, dto.IsRushJob, editOvenRate, job.OvenBatches, job.OvenCycleMinutes);
job.FinalPrice = totals.Total; job.FinalPrice = totals.Total;
job.OvenBatchCost = totals.OvenBatchCost; job.OvenBatchCost = totals.OvenBatchCost;
@@ -2930,7 +2930,7 @@ public class JobsController : Controller
JobId = job.Id, JobId = job.Id,
JobNumber = job.JobNumber, JobNumber = job.JobNumber,
CustomerId = job.CustomerId, CustomerId = job.CustomerId,
TaxPercent = costs?.TaxPercent ?? 0m, TaxPercent = await GetEffectiveTaxPercentAsync(job.CustomerId, costs?.TaxPercent ?? 0m),
OvenCostId = job.OvenCostId, OvenCostId = job.OvenCostId,
OvenBatches = job.OvenBatches > 0 ? job.OvenBatches : 1, OvenBatches = job.OvenBatches > 0 ? job.OvenBatches : 1,
OvenCycleMinutes = job.OvenCycleMinutes, OvenCycleMinutes = job.OvenCycleMinutes,
@@ -2967,7 +2967,7 @@ public class JobsController : Controller
{ {
ModelState.AddModelError("", "Please add at least one job item."); ModelState.AddModelError("", "Please add at least one job item.");
var costs = await _pricingService.GetOperatingCostsAsync(currentUser.CompanyId); var costs = await _pricingService.GetOperatingCostsAsync(currentUser.CompanyId);
model.TaxPercent = costs?.TaxPercent ?? 0m; model.TaxPercent = await GetEffectiveTaxPercentAsync(job.CustomerId, costs?.TaxPercent ?? 0m);
await PopulateJobItemDropDownsAsync(currentUser.CompanyId, costs?.OvenOperatingCostPerHour ?? 45m); await PopulateJobItemDropDownsAsync(currentUser.CompanyId, costs?.OvenOperatingCostPerHour ?? 45m);
ViewBag.ComplexitySimplePercent = costs?.ComplexitySimplePercent ?? 0m; ViewBag.ComplexitySimplePercent = costs?.ComplexitySimplePercent ?? 0m;
ViewBag.ComplexityModeratePercent = costs?.ComplexityModeratePercent ?? 5m; ViewBag.ComplexityModeratePercent = costs?.ComplexityModeratePercent ?? 5m;
@@ -3020,9 +3020,11 @@ public class JobsController : Controller
if (oven != null && oven.CompanyId == currentUser.CompanyId) if (oven != null && oven.CompanyId == currentUser.CompanyId)
ovenRateOverride = oven.CostPerHour; ovenRateOverride = oven.CostPerHour;
} }
var updateCosts = await _pricingService.GetOperatingCostsAsync(currentUser.CompanyId);
var totals = await _pricingService.CalculateQuoteTotalsAsync( var totals = await _pricingService.CalculateQuoteTotalsAsync(
model.JobItems, currentUser.CompanyId, job.CustomerId, model.JobItems, currentUser.CompanyId, job.CustomerId,
model.TaxPercent, job.DiscountType.ToString(), job.DiscountValue, job.IsRushJob, await GetEffectiveTaxPercentAsync(job.CustomerId, updateCosts?.TaxPercent ?? 0m),
job.DiscountType.ToString(), job.DiscountValue, job.IsRushJob,
ovenRateOverride, job.OvenBatches, job.OvenCycleMinutes); ovenRateOverride, job.OvenBatches, job.OvenCycleMinutes);
job.FinalPrice = totals.Total; job.FinalPrice = totals.Total;
@@ -3043,7 +3045,7 @@ public class JobsController : Controller
_logger.LogError(ex, "Error updating items for job {JobId}", job.Id); _logger.LogError(ex, "Error updating items for job {JobId}", job.Id);
TempData["Error"] = "An error occurred while saving job items."; TempData["Error"] = "An error occurred while saving job items.";
var costs = await _pricingService.GetOperatingCostsAsync(currentUser.CompanyId); var costs = await _pricingService.GetOperatingCostsAsync(currentUser.CompanyId);
model.TaxPercent = costs?.TaxPercent ?? 0m; model.TaxPercent = await GetEffectiveTaxPercentAsync(job.CustomerId, costs?.TaxPercent ?? 0m);
await PopulateJobItemDropDownsAsync(currentUser.CompanyId, costs?.OvenOperatingCostPerHour ?? 45m); await PopulateJobItemDropDownsAsync(currentUser.CompanyId, costs?.OvenOperatingCostPerHour ?? 45m);
return View("EditItems", model); return View("EditItems", model);
} }
@@ -3110,7 +3112,7 @@ public class JobsController : Controller
} }
var totals = await _pricingService.CalculateQuoteTotalsAsync( var totals = await _pricingService.CalculateQuoteTotalsAsync(
remainingDtos, currentUser.CompanyId, job.CustomerId, remainingDtos, currentUser.CompanyId, job.CustomerId,
costs?.TaxPercent ?? 0m, await GetEffectiveTaxPercentAsync(job.CustomerId, costs?.TaxPercent ?? 0m),
job.DiscountType.ToString(), job.DiscountValue, job.IsRushJob, job.DiscountType.ToString(), job.DiscountValue, job.IsRushJob,
deleteOvenRate, job.OvenBatches, job.OvenCycleMinutes); deleteOvenRate, job.OvenBatches, job.OvenCycleMinutes);
job.FinalPrice = totals.Total; job.FinalPrice = totals.Total;
@@ -3239,6 +3241,21 @@ public class JobsController : Controller
/// Converts a <see cref="QuotePricingResult"/> into the DTO used for both display and JSON snapshot storage. /// Converts a <see cref="QuotePricingResult"/> into the DTO used for both display and JSON snapshot storage.
/// All save paths (Create, Edit, UpdateItems, DeleteJobItem) call this so the snapshot is always consistent. /// All save paths (Create, Edit, UpdateItems, DeleteJobItem) call this so the snapshot is always consistent.
/// </summary> /// </summary>
/// <summary>
/// Returns the effective tax rate for a job, respecting customer tax-exempt status.
/// Always call this instead of using costs.TaxPercent directly so tax-exempt customers
/// are never charged tax when a job is saved or recalculated.
/// </summary>
private async Task<decimal> GetEffectiveTaxPercentAsync(int? customerId, decimal companyDefaultRate)
{
if (customerId is > 0)
{
var customer = await _unitOfWork.Customers.GetByIdAsync(customerId.Value);
if (customer?.IsTaxExempt == true) return 0m;
}
return companyDefaultRate;
}
private static QuotePricingBreakdownDto BuildPricingSnapshotDto(QuotePricingResult pr) => private static QuotePricingBreakdownDto BuildPricingSnapshotDto(QuotePricingResult pr) =>
new QuotePricingBreakdownDto new QuotePricingBreakdownDto
{ {
+16 -16
View File
@@ -321,7 +321,7 @@
<div class="card-body"> <div class="card-body">
@* ── Catalog Products ── *@ @* -- Catalog Products -- *@
@if (catalogItems.Any()) @if (catalogItems.Any())
{ {
<h6 class="text-primary mb-3"><i class="bi bi-bag-check me-2"></i>Catalog Products</h6> <h6 class="text-primary mb-3"><i class="bi bi-bag-check me-2"></i>Catalog Products</h6>
@@ -414,7 +414,7 @@
</div> </div>
} }
@* ── Custom Work ── *@ @* -- Custom Work -- *@
@if (customItems.Any()) @if (customItems.Any())
{ {
<h6 class="text-success mb-3"><i class="bi bi-calculator me-2"></i>Custom Work</h6> <h6 class="text-success mb-3"><i class="bi bi-calculator me-2"></i>Custom Work</h6>
@@ -565,7 +565,7 @@
</div> </div>
} }
@* ── Labor ── *@ @* -- Labor -- *@
@if (laborItems.Any()) @if (laborItems.Any())
{ {
<h6 class="text-warning mb-3"><i class="bi bi-person-gear me-2"></i>Labor</h6> <h6 class="text-warning mb-3"><i class="bi bi-person-gear me-2"></i>Labor</h6>
@@ -616,7 +616,7 @@
</div> </div>
} }
@* ── Mobile cards ── *@ @* -- Mobile cards -- *@
<div class="d-lg-none mt-2"> <div class="d-lg-none mt-2">
@foreach (var item in Model.Items) @foreach (var item in Model.Items)
{ {
@@ -1310,7 +1310,7 @@
<a asp-action="Intake" asp-route-id="@Model.Id" <a asp-action="Intake" asp-route-id="@Model.Id"
class="btn @(Model.IntakeDate.HasValue ? "btn-outline-secondary" : "btn-outline-info")" class="btn @(Model.IntakeDate.HasValue ? "btn-outline-secondary" : "btn-outline-info")"
title="@(Model.IntakeDate.HasValue ? "Update part intake record" : "Check in parts for this job")"> title="@(Model.IntakeDate.HasValue ? "Update part intake record" : "Check in parts for this job")">
<i class="bi bi-box-seam me-2"></i>@(Model.IntakeDate.HasValue ? "Intake &#10003;" : "Intake") <i class="bi bi-box-seam me-2"></i>@Html.Raw(Model.IntakeDate.HasValue ? "Intake &#10003;" : "Intake")
</a> </a>
} }
@{ @{
@@ -2332,7 +2332,7 @@
<script src="~/js/job-photos.js" asp-append-version="true"></script> <script src="~/js/job-photos.js" asp-append-version="true"></script>
<script src="~/js/customer-change.js" asp-append-version="true"></script> <script src="~/js/customer-change.js" asp-append-version="true"></script>
<script> <script>
// ── Inline date editing ────────────────────────────────────────────── // -- Inline date editing ----------------------------------------------
const jobId = @Model.Id; const jobId = @Model.Id;
const antiForgeryToken = () => document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? ''; const antiForgeryToken = () => document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
@@ -2433,7 +2433,7 @@
jobPhotoModule.init(@Model.Id, @Html.Raw(ViewBag.PhotoTagSuggestions ?? "[]")); jobPhotoModule.init(@Model.Id, @Html.Raw(ViewBag.PhotoTagSuggestions ?? "[]"));
// ── Auto-submit after wizard saves an item ──────────────────────── // -- Auto-submit after wizard saves an item ------------------------
let itemsModified = false; let itemsModified = false;
// Wrap wizardSave to set a flag before the modal hides // Wrap wizardSave to set a flag before the modal hides
@@ -2451,7 +2451,7 @@
} }
}); });
// ── Delete confirmation modal ───────────────────────────────────── // -- Delete confirmation modal -------------------------------------
let pendingDeleteItemId = -1; let pendingDeleteItemId = -1;
const deleteModal = new bootstrap.Modal(document.getElementById('deleteConfirmModal')); const deleteModal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
const deleteItemToken = document.querySelector('input[name="__RequestVerificationToken"]').value; const deleteItemToken = document.querySelector('input[name="__RequestVerificationToken"]').value;
@@ -2489,7 +2489,7 @@
}); });
</script> </script>
<!-- ── Rework / Warranty ────────────────────────────────────────────── --> <!-- -- Rework / Warranty ---------------------------------------------- -->
<script> <script>
const rework = (() => { const rework = (() => {
const jid = @Model.Id; const jid = @Model.Id;
@@ -2645,7 +2645,7 @@
})(); })();
</script> </script>
<!-- ── Job Costing ──────────────────────────────────────────────────── --> <!-- -- Job Costing ---------------------------------------------------- -->
<script> <script>
const costing = (() => { const costing = (() => {
const jid = @Model.Id; const jid = @Model.Id;
@@ -2754,7 +2754,7 @@
})(); })();
</script> </script>
<!-- ── Time Tracking ─────────────────────────────────────────────────── --> <!-- -- Time Tracking --------------------------------------------------- -->
<script> <script>
const timeTracking = (() => { const timeTracking = (() => {
const jid = @Model.Id; const jid = @Model.Id;
@@ -2762,7 +2762,7 @@
const modal = new bootstrap.Modal(document.getElementById('timeEntryModal')); const modal = new bootstrap.Modal(document.getElementById('timeEntryModal'));
let entries = []; let entries = [];
// ── Load ────────────────────────────────────────────────────────── // -- Load ----------------------------------------------------------
async function load() { async function load() {
const r = await fetch(`/Jobs/GetTimeEntries?jobId=${jid}`); const r = await fetch(`/Jobs/GetTimeEntries?jobId=${jid}`);
entries = await r.json(); entries = await r.json();
@@ -2810,7 +2810,7 @@
document.getElementById('timeEntriesTotalHours').textContent = total > 0 ? total.toFixed(2) : '—'; document.getElementById('timeEntriesTotalHours').textContent = total > 0 ? total.toFixed(2) : '—';
} }
// ── Modal helpers ───────────────────────────────────────────────── // -- Modal helpers -------------------------------------------------
function openAdd() { function openAdd() {
document.getElementById('timeEntryModalTitle').textContent = 'Log Time'; document.getElementById('timeEntryModalTitle').textContent = 'Log Time';
document.getElementById('teEntryId').value = '0'; document.getElementById('teEntryId').value = '0';
@@ -2917,7 +2917,7 @@
} }
}); });
// ── Deposits ───────────────────────────────────────────────────────────── // -- Deposits -------------------------------------------------------------
// Note: antiForgeryToken() is already defined above in this script block // Note: antiForgeryToken() is already defined above in this script block
document.getElementById('addDepositForm')?.addEventListener('submit', async function(e) { document.getElementById('addDepositForm')?.addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
@@ -2973,7 +2973,7 @@
} }
} }
// ── Collapsible sections ────────────────────────────────────────────────── // -- Collapsible sections --------------------------------------------------
(function () { (function () {
const storageKey = 'jobDetailCollapse_@Model.Id'; const storageKey = 'jobDetailCollapse_@Model.Id';
const sections = ['collapseTimeTracking', 'collapsePartIntake', 'collapsePhotos', 'collapseDeposits', 'collapseMaterials']; const sections = ['collapseTimeTracking', 'collapsePartIntake', 'collapsePhotos', 'collapseDeposits', 'collapseMaterials'];
@@ -3012,7 +3012,7 @@
}); });
})(); })();
// ── Part Intake Modal ───────────────────────────────────────────────────── // -- Part Intake Modal --------------------------------------------------
(function () { (function () {
const expectedCount = @intakeExpectedCount; const expectedCount = @intakeExpectedCount;
const partCountInput = document.getElementById('intakePartCount'); const partCountInput = document.getElementById('intakePartCount');
@@ -604,6 +604,8 @@
if (taxField) { if (taxField) {
taxField.value = exemptIds.has(customerId) ? 0 : (meta.companyTaxPercent ?? meta.taxPercent); taxField.value = exemptIds.has(customerId) ? 0 : (meta.companyTaxPercent ?? meta.taxPercent);
} }
// Recalculate the live preview so it reflects the updated tax rate immediately
if (typeof scheduleAutoPricing === 'function') scheduleAutoPricing();
const noEmail = customerId > 0 && noEmailIds.has(customerId); const noEmail = customerId > 0 && noEmailIds.has(customerId);
const emailSection = document.getElementById('emailNotifySection'); const emailSection = document.getElementById('emailNotifySection');
@@ -2904,7 +2904,8 @@ async function runAutoPricing() {
try { try {
// Collect current form meta // Collect current form meta
const customerId = parseInt(document.querySelector('[name="CustomerId"]')?.value) || null; const customerId = parseInt(document.querySelector('[name="CustomerId"]')?.value) || null;
const taxPercent = parseFloat(document.querySelector('[name="TaxPercent"]')?.value) || pageMeta.taxPercent || 0; const _taxField = document.querySelector('[name="TaxPercent"]');
const taxPercent = _taxField ? parseFloat(_taxField.value) : (pageMeta.taxPercent ?? 0);
const discountType = document.getElementById('discountTypeSelect')?.value || 'None'; const discountType = document.getElementById('discountTypeSelect')?.value || 'None';
const discountVal = parseFloat(document.getElementById('discountValueInput')?.value) || 0; const discountVal = parseFloat(document.getElementById('discountValueInput')?.value) || 0;
const isRushJob = document.getElementById('IsRushJob')?.checked || false; const isRushJob = document.getElementById('IsRushJob')?.checked || false;