Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7020797a25 | |||
| 3b5511a703 | |||
| 8df37ca760 | |||
| 7239f55308 | |||
| 09e077897b | |||
| 051c86810e |
src/PowderCoating.Infrastructure/Migrations/20260515194344_AddQuotePricingSnapshotFields.Designer.cs
Generated
+10778
File diff suppressed because it is too large
Load Diff
+138
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 ✓" : "Intake")
|
<i class="bi bi-box-seam me-2"></i>@Html.Raw(Model.IntakeDate.HasValue ? "Intake ✓" : "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;
|
||||||
|
|||||||
Reference in New Issue
Block a user