@model PowderCoating.Application.DTOs.Quote.QuoteDto @{ ViewData["Title"] = $"Quote {Model.QuoteNumber}"; ViewData["PageIcon"] = "bi-file-text"; var guidedActivationCallout = ViewBag.GuidedActivationCallout as PowderCoating.Web.ViewModels.GuidedActivation.GuidedActivationCalloutViewModel; var guidedActivationMode = ViewBag.GuidedActivationMode as string; }

@Model.StatusDisplayName @if (Model.IsExpired) { Expired }

Help Edit @if (Model.IsProspect && Model.StatusCode == "APPROVED") { Convert } Back
@if (guidedActivationCallout?.Show == true) {
@guidedActivationCallout.Title
@guidedActivationCallout.Message
@Html.AntiForgeryToken()
}
@if (Model.IsProspect) { @:Prospect/Walk-In Information } else { @:Customer Information }
@if (Model.IsProspect) {

Company Name: @(Model.ProspectCompanyName ?? "-")

Contact Name: @(Model.ProspectContactName ?? "-")

Email: @(Model.ProspectEmail ?? "-")

Phone: @(Model.ProspectPhone ?? "-")

SMS Consent: @if (Model.ProspectSmsConsent) { Consented @if (Model.ProspectSmsConsentedAt.HasValue) { on @Model.ProspectSmsConsentedAt.Value.ToLocalTime().ToString("MM/dd/yyyy") } } else { Not recorded }

Address: @(Model.ProspectAddress ?? "-")

City: @(Model.ProspectCity ?? "-")

State: @(Model.ProspectState ?? "-")

Zip Code: @(Model.ProspectZipCode ?? "-")

} else {
@Html.AntiForgeryToken() Customer:
}
Quote Details

Quote Date: @Model.QuoteDate.ToString("MM/dd/yyyy")

Expiration Date: @(Model.ExpirationDate?.ToString("MM/dd/yyyy") ?? "-")

Prepared By: @if (!string.IsNullOrEmpty(Model.PreparedById)) { } @(Model.PreparedByName ?? "-")

Type: @(Model.IsCommercial ? "Commercial" : "Non-Commercial")

@if (Model.SentDate.HasValue) {

Sent Date: @Model.SentDate.Value.ToString("MM/dd/yyyy")

} @if (Model.ApprovedDate.HasValue) {

Approved Date: @Model.ApprovedDate.Value.ToString("MM/dd/yyyy")

} @if (Model.ApprovalTokenUsedAt.HasValue && Model.StatusCode == "REJECTED") {

Declined On: @Model.ApprovalTokenUsedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yyyy h:mm tt")

} @if (!string.IsNullOrEmpty(Model.CustomerPO)) {

Customer PO: @Model.CustomerPO

} @if (!string.IsNullOrEmpty(Model.ProjectName)) {

Project: @Model.ProjectName

}
@if (!string.IsNullOrEmpty(Model.Description)) {

Description:

@Model.Description

} @if (!string.IsNullOrEmpty(Model.Terms)) {

Terms & Conditions:

@Model.Terms

} @if (!string.IsNullOrEmpty(Model.Notes)) {

Internal Notes:

@Model.Notes

} @if (!string.IsNullOrWhiteSpace(Model.Tags)) {
@foreach (var tag in Model.Tags.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()).Where(t => !string.IsNullOrWhiteSpace(t))) { @tag }
} @if (!string.IsNullOrEmpty(Model.DeclineReason)) {
Customer Declined

@Model.DeclineReason

} @if (Model.PrepServices != null && Model.PrepServices.Any()) {

Preparation Services:

@foreach (var service in Model.PrepServices) { @service.ServiceName }
}
Quote Items
@if (Model.QuoteItems != null && Model.QuoteItems.Any(i => i.Description != null && i.Description.StartsWith("Custom Powder Order"))) { } @if (Model.QuoteItems != null && Model.QuoteItems.Any()) { var catalogItems = Model.QuoteItems.Where(i => i.CatalogItemId.HasValue).ToList(); var calculatedItems = Model.QuoteItems.Where(i => !i.CatalogItemId.HasValue).ToList(); @* Catalog Items Section *@ @if (catalogItems.Any()) {
Catalog Products
@foreach (var item in catalogItems) { }
Product Qty Unit Price Total
@{ var displayDescription = item.Description == "Product Item" || string.IsNullOrWhiteSpace(item.Description) ? (item.CatalogItemName ?? "Catalog Item") : item.Description; } @displayDescription @if (item.CatalogItemId.HasValue && item.Description != "Product Item" && !string.IsNullOrWhiteSpace(item.Description)) {
@item.CatalogItemName } @* Display coating layers *@ @if (item.Coats != null && item.Coats.Any()) {
Coating Layers: @foreach (var coat in item.Coats.OrderBy(c => c.Sequence)) {
@coat.CoatName @if (!string.IsNullOrEmpty(coat.ColorName)) { - @coat.ColorName @if (!string.IsNullOrEmpty(coat.VendorName)) { (@coat.VendorName) } } @if (!string.IsNullOrEmpty(coat.ColorCode)) { (@coat.ColorCode) } @if (coat.InventoryItemId.HasValue && !string.IsNullOrEmpty(coat.InventoryItemName)) { @coat.InventoryItemName } @if (coat.PowderToOrder.HasValue && coat.PowderToOrder.Value > 0) { @if (coat.InventoryItemId.HasValue) { Need: @coat.PowderToOrder lbs } else { ORDER: @coat.PowderToOrder?.ToString("0.##") lbs } } @if (!string.IsNullOrEmpty(coat.Notes)) {
@coat.Notes }
} } @if (!string.IsNullOrEmpty(item.Notes)) {
Notes: @item.Notes }
@item.Quantity @item.UnitPrice.ToString("C") @item.TotalPrice.ToString("C")
} @* Calculated Items Section *@ @if (calculatedItems.Any()) {
Custom Work
@foreach (var item in calculatedItems) { // Calculate powder needed for this item var coverageRate = 30m; // sq ft per lb (default estimate) var transferEff = 0.65m; // 65% efficiency (default estimate) var totalSqFt = item.SurfaceAreaSqFt * item.Quantity; var powderOnPart = totalSqFt / coverageRate; var totalPowderNeeded = powderOnPart / transferEff; }
Description Qty Surface Area Est. Minutes Coating Needed Unit Price Total
@item.Description @* Display coating layers *@ @if (item.Coats != null && item.Coats.Any()) {
Coating Layers: @foreach (var coat in item.Coats.OrderBy(c => c.Sequence)) {
@coat.CoatName @if (!string.IsNullOrEmpty(coat.ColorName)) { - @coat.ColorName @if (!string.IsNullOrEmpty(coat.VendorName)) { (@coat.VendorName) } } @if (!string.IsNullOrEmpty(coat.ColorCode)) { (@coat.ColorCode) } @if (coat.InventoryItemId.HasValue && !string.IsNullOrEmpty(coat.InventoryItemName)) { @coat.InventoryItemName } @if (coat.PowderToOrder.HasValue && coat.PowderToOrder.Value > 0) { @if (coat.InventoryItemId.HasValue) { Need: @coat.PowderToOrder lbs } else { ORDER: @coat.PowderToOrder?.ToString("0.##") lbs } } @if (!string.IsNullOrEmpty(coat.Notes)) {
@coat.Notes }
} } @if (item.PrepServices != null && item.PrepServices.Any()) {
Prep Services: @foreach (var ps in item.PrepServices) {
@(ps.PrepServiceName ?? $"Service #{ps.PrepServiceId}") — @ps.EstimatedMinutes min } } @if (!string.IsNullOrEmpty(item.Notes)) {
Notes: @item.Notes }
@item.Quantity @item.SurfaceAreaSqFt.ToString("F2") @ViewBag.AreaUnit
per item
@item.EstimatedMinutes min
per item
@totalPowderNeeded.ToString("F2") lbs
total batch
@item.UnitPrice.ToString("C") @item.TotalPrice.ToString("C")
} } else {

No items in this quote.

}
@if (Model.QuoteItems != null && Model.QuoteItems.Any()) { var mobileCatalogItems = Model.QuoteItems.Where(i => i.CatalogItemId.HasValue).ToList(); var mobileCalculatedItems = Model.QuoteItems.Where(i => !i.CatalogItemId.HasValue).ToList(); @* Catalog Items Mobile Section *@ @if (mobileCatalogItems.Any()) {
Catalog Products
@foreach (var item in mobileCatalogItems) {
@{ var displayDesc = item.Description == "Product Item" || string.IsNullOrWhiteSpace(item.Description) ? (item.CatalogItemName ?? "Catalog Item") : item.Description; }
@displayDesc
@if (item.CatalogItemId.HasValue && item.Description != "Product Item" && !string.IsNullOrWhiteSpace(item.Description)) { @item.CatalogItemName }
Quantity @item.Quantity
Unit Price @item.UnitPrice.ToString("C")
Total Price @item.TotalPrice.ToString("C")
@if (!string.IsNullOrEmpty(item.Notes)) {
Notes @item.Notes
}
}
} @* Calculated Items Mobile Section *@ @if (mobileCalculatedItems.Any()) {
Custom Work
@foreach (var item in mobileCalculatedItems) { // Calculate powder needed for mobile view var mobileCoverageRate = 30m; var mobileTransferEff = 0.65m; var mobileTotalSqFt = item.SurfaceAreaSqFt * item.Quantity; var mobilePowderOnPart = mobileTotalSqFt / mobileCoverageRate; var mobileTotalPowderNeeded = mobilePowderOnPart / mobileTransferEff;
@item.Description
@(item.Coats.FirstOrDefault()?.ColorName ?? "No color specified")
Quantity @item.Quantity
Surface Area @item.SurfaceAreaSqFt.ToString("F2") @ViewBag.AreaUnit (per item)
Est. Time @item.EstimatedMinutes min (per item)
Coating Needed @mobileTotalPowderNeeded.ToString("F2") lbs (batch)
@if (item.Coats != null && item.Coats.Any()) {
Coating Layers @foreach (var coat in item.Coats.OrderBy(c => c.Sequence)) {
@coat.CoatName @if (!string.IsNullOrEmpty(coat.ColorName)) { - @coat.ColorName } @if (!string.IsNullOrEmpty(coat.ColorCode)) { (@coat.ColorCode) } @if (coat.InventoryItemId.HasValue && !string.IsNullOrEmpty(coat.InventoryItemName)) {
@coat.InventoryItemName } @if (coat.PowderToOrder.HasValue && coat.PowderToOrder.Value > 0) {
@if (coat.InventoryItemId.HasValue) { Need: @coat.PowderToOrder lbs } else { Order: @coat.PowderToOrder lbs } }
}
} @if (item.PrepServices != null && item.PrepServices.Any()) {
Prep Services @foreach (var ps in item.PrepServices) {
@(ps.PrepServiceName ?? $"Service #{ps.PrepServiceId}") — @ps.EstimatedMinutes min
}
}
Unit Price @item.UnitPrice.ToString("C")
Total Price @item.TotalPrice.ToString("C")
@if (!string.IsNullOrEmpty(item.Notes)) {
Notes @item.Notes
}
}
} }
@{ var allPhotos = ViewBag.QuotePhotos as List ?? new List(); bool canUpload = ViewBag.CanUploadQuotePhoto is bool b && b; int photoUsed = ViewBag.QuotePhotoUsed is int u ? u : 0; int photoMax = ViewBag.QuotePhotoMax is int m ? m : -1; } @if (photoMax != 0) {
Photos @allPhotos.Count
@if (photoMax > 0) { @photoUsed / @photoMax } @if (canUpload) { } else if (photoMax > 0 && photoUsed >= photoMax) { Limit reached }
@if (!allPhotos.Any()) {

No photos uploaded yet.

}
@for (int pi = 0; pi < allPhotos.Count; pi++) { var photo = allPhotos[pi];
@if (photo.IsAiAnalysisPhoto) { AI } @photo.FileName
}
Uploading…
}
Pricing Summary
@if (!string.IsNullOrWhiteSpace(Model.OvenLabel)) {
Oven @Model.OvenLabel
}
Items Subtotal: @Model.PricingBreakdown.ItemsSubtotal.ToString("C")
@if (Model.PricingBreakdown.OvenBatchCost > 0) {
Oven (@Model.PricingBreakdown.OvenBatches batch@(Model.PricingBreakdown.OvenBatches != 1 ? "es" : "")@(Model.PricingBreakdown.OvenCycleMinutes > 0 ? $" × {Model.PricingBreakdown.OvenCycleMinutes} min" : "")): @Model.PricingBreakdown.OvenBatchCost.ToString("C")
} @if (Model.PricingBreakdown.FacilityOverheadCost > 0) {
Facility Overhead (@Model.PricingBreakdown.FacilityOverheadRatePerHour.ToString("C2")/hr): @Model.PricingBreakdown.FacilityOverheadCost.ToString("C")
} @if (Model.PricingBreakdown.ShopSuppliesAmount > 0) {
Shop Supplies (@Model.PricingBreakdown.ShopSuppliesPercent%): @Model.PricingBreakdown.ShopSuppliesAmount.ToString("C")
}
Subtotal: @Model.PricingBreakdown.SubtotalBeforeDiscount.ToString("C")
@if (Model.PricingBreakdown.DiscountAmount > 0) {
@if (Model.DiscountType == "Percentage") { Discount (@Model.DiscountValue% Off): } else if (Model.DiscountType == "FixedAmount") { Discount (@Model.DiscountValue.ToString("C") Off): } else { Discount (@Model.PricingBreakdown.DiscountPercent.ToString("F1")%): } -@Model.PricingBreakdown.DiscountAmount.ToString("C")
@if (!string.IsNullOrWhiteSpace(Model.DiscountReason)) {
Reason: @Model.DiscountReason
} @if (Model.HideDiscountFromCustomer) {
Discount hidden from customer on PDFs and approval portal
} } @if (Model.IsRushJob && Model.PricingBreakdown.RushFee > 0) {
Rush Job Fee: @Model.PricingBreakdown.RushFee.ToString("C")
} @if (Model.PricingBreakdown.TaxAmount > 0) {
Tax (@Model.PricingBreakdown.TaxPercent.ToString("G29")%): @Model.PricingBreakdown.TaxAmount.ToString("C")
}
Total:
@Model.Total.ToString("C")
@if (Model.PricingBreakdown.DiscountAmount > 0) {
You Save: @Model.PricingBreakdown.DiscountAmount.ToString("C")
} @if (Model.PricingBreakdown != null) {
@{ var pb = Model.PricingBreakdown; var dbgCosts = ViewBag.OperatingCosts as PowderCoating.Core.Entities.CompanyOperatingCosts; var directCosts = pb.MaterialCosts + pb.LaborCosts + pb.EquipmentCosts; var hasCostBreakdown = pb.MaterialCosts > 0 || pb.LaborCosts > 0 || pb.EquipmentCosts > 0; var allCatalog = Model.QuoteItems != null && Model.QuoteItems.All(i => i.CatalogItemId.HasValue); } @* -- SECTION 1: Item Cost Breakdown ----------------------- *@
Item Costs
@if (hasCostBreakdown) {
Material (powder + consumables) @pb.MaterialCosts.ToString("C")
Labor @pb.LaborCosts.ToString("C")
Equipment (oven + booth) @pb.EquipmentCosts.ToString("C")
Direct costs @directCosts.ToString("C")
Markup (@pb.ProfitPercent.ToString("F0")% baked into item prices) @((pb.ItemsSubtotal - directCosts).ToString("C"))
} else if (allCatalog) {
All items use fixed catalog pricing — no per-category cost split available.
} else {
Cost breakdown not available for this quote (saved before tracking was added).
}
Items subtotal @pb.ItemsSubtotal.ToString("C")
@* -- SECTION 2: Quote-Level Additions --------------------- *@ @if (pb.OvenBatchCost > 0 || pb.FacilityOverheadCost > 0 || pb.ShopSuppliesAmount > 0 || pb.OverheadCosts > 0) {
Quote-Level Additions
@if (pb.OvenBatchCost > 0) {
Oven batch (@pb.OvenBatches batch@(pb.OvenBatches != 1 ? "es" : "")@(pb.OvenCycleMinutes > 0 ? $", {pb.OvenCycleMinutes} min/cycle" : "")) @pb.OvenBatchCost.ToString("C")
} @if (pb.FacilityOverheadCost > 0) {
Facility overhead (@pb.FacilityOverheadRatePerHour.ToString("C2")/hr × estimated hours) @pb.FacilityOverheadCost.ToString("C")
} @if (pb.ShopSuppliesAmount > 0) {
Shop supplies (@pb.ShopSuppliesPercent.ToString("F1")%) @pb.ShopSuppliesAmount.ToString("C")
} @if (pb.OverheadCosts > 0) {
Overhead (@pb.OverheadPercent.ToString("F1")%) @pb.OverheadCosts.ToString("C")
}
} @* -- SECTION 3: Final Calculation -------------------------- *@
Final Calculation
Subtotal @pb.SubtotalBeforeDiscount.ToString("C")
@if (pb.DiscountAmount > 0) {
Discount (@pb.DiscountPercent.ToString("F1")%) -@pb.DiscountAmount.ToString("C")
After discount @pb.SubtotalAfterDiscount.ToString("C")
} @if (pb.RushFee > 0) {
Rush fee @pb.RushFee.ToString("C")
} @if (pb.TaxAmount > 0) {
Tax (@pb.TaxPercent.ToString("G29")%) @pb.TaxAmount.ToString("C")
}
Total @pb.Total.ToString("C")
@{ var totalDirectCost = pb.MaterialCosts + pb.LaborCosts + pb.EquipmentCosts + pb.OvenBatchCost + pb.FacilityOverheadCost + pb.ShopSuppliesAmount; var grossProfit = pb.Total - totalDirectCost; var effectiveMargin = pb.Total > 0 ? (grossProfit / pb.Total * 100m) : 0m; var pricingModeLabel = dbgCosts?.PricingMode == PowderCoating.Core.Enums.PricingMode.MarginOnTotalCost ? "margin" : "markup"; } @if (totalDirectCost > 0) {
Effective gross margin @effectiveMargin.ToString("F1")%
@if (Model.DiscountAmount > 0) { var priceBeforeDiscount = pb.Total + pb.DiscountAmount; var marginBeforeDiscount = priceBeforeDiscount > 0 ? ((priceBeforeDiscount - totalDirectCost) / priceBeforeDiscount * 100m) : 0m;
Without discount: @marginBeforeDiscount.ToString("F1")% — discount cost you @((marginBeforeDiscount - effectiveMargin).ToString("F1")) margin points
} }
@* -- SECTION 4: Per-Item Cost Breakdown ------------------- *@ @{ var hasItemCostData = Model.QuoteItems != null && Model.QuoteItems.Any(i => i.ItemMaterialCost > 0 || i.ItemLaborCost > 0 || i.ItemEquipmentCost > 0); var markupPct = dbgCosts?.GeneralMarkupPercentage ?? pb.ProfitPercent; var laborRate = dbgCosts?.StandardLaborRate ?? 0m; var boothRate = dbgCosts?.CoatingBoothCostPerHour ?? 0m; } @if (Model.QuoteItems != null && Model.QuoteItems.Any()) {
@* Customer tier alert *@ @if (ViewBag.PricingTier != null) { var tier = ViewBag.PricingTier as PowderCoating.Core.Entities.PricingTier;
Pricing Tier: @tier.TierName — @tier.DiscountPercent% discount applied to this customer
} @if (!hasItemCostData) {
Per-item cost detail not available — re-save this quote to capture the breakdown.
} @* Per-item breakdown cards *@ @foreach (var item in Model.QuoteItems) { var rawPowder = item.Coats.Sum(c => c.CoatMaterialCost); var consumables = item.ItemMaterialCost - rawPowder; var markupAmt = item.ItemMaterialCost * (markupPct / 100m); var coatingLabor = item.Coats.Sum(c => c.CoatLaborCost); var prepLabor = item.ItemLaborCost - coatingLabor; var prepMinutes = item.PrepServices?.Sum(ps => ps.EstimatedMinutes) ?? 0; var boothHours = boothRate > 0 ? item.ItemEquipmentCost / boothRate : 0m; var hasDetail = item.ItemMaterialCost > 0 || item.ItemLaborCost > 0 || item.ItemEquipmentCost > 0;
@* Item header *@
@(item.Description ?? item.CatalogItemName ?? "(no description)") @if (item.CatalogItemId.HasValue) { Catalog } @if (item.IsAiItem) { AI } @if (item.IsCustomFormulaItem) { Formula } @if (item.SurfaceAreaSqFt > 0 || item.EstimatedMinutes > 0) { @if (item.SurfaceAreaSqFt > 0) { @item.SurfaceAreaSqFt.ToString("F2") sqft } @if (item.SurfaceAreaSqFt > 0 && item.EstimatedMinutes > 0) { · } @if (item.EstimatedMinutes > 0) { @item.EstimatedMinutes min } }
@if (item.Quantity > 1) { ×@item.Quantity.ToString("G29") } @item.TotalPrice.ToString("C")
@if (hasDetail) {
@* MATERIAL column *@ @if (item.ItemMaterialCost > 0 || rawPowder > 0) {
Material
@if (rawPowder > 0) {
Powder (raw) @rawPowder.ToString("C")
} @if (consumables > 0.001m) {
Consumables (+5%) @consumables.ToString("C")
} @if (markupAmt > 0.001m) {
Markup (+@markupPct.ToString("F0")%) @markupAmt.ToString("C")
}
Material total @((item.ItemMaterialCost + markupAmt).ToString("C"))
} @* LABOR column *@ @if (item.ItemLaborCost > 0) {
Labor @if (laborRate > 0) { @@ @laborRate.ToString("C0")/hr }
@if (coatingLabor > 0) {
Coating @if (item.EstimatedMinutes > 0) { (@item.EstimatedMinutes min) } @coatingLabor.ToString("C")
} @if (prepLabor > 0.001m) {
Prep services @if (prepMinutes > 0) { (@prepMinutes min) } @prepLabor.ToString("C")
}
Labor total @item.ItemLaborCost.ToString("C")
} @* EQUIPMENT column *@ @if (item.ItemEquipmentCost > 0) {
Equipment
Coating booth @if (boothRate > 0 && boothHours > 0) { (@boothHours.ToString("F2") hrs @@ @boothRate.ToString("C0")/hr) } @item.ItemEquipmentCost.ToString("C")
Equipment total @item.ItemEquipmentCost.ToString("C")
}
@* Coat detail rows *@ @if (item.Coats.Any()) {
Coat Layers
@foreach (var coat in item.Coats.OrderBy(c => c.Sequence)) {
@coat.CoatName @if (!string.IsNullOrEmpty(coat.ColorName)) { — @coat.ColorName } @if (!string.IsNullOrEmpty(coat.ColorCode)) { (@coat.ColorCode) } @if (coat.InventoryItemId.HasValue) { Stock powder } else if (coat.PowderToOrder > 0) { Order @coat.PowderToOrder?.ToString("0.##") lbs }
@if (coat.CoatMaterialCost > 0) { @coat.CoatMaterialCost.ToString("C") mat } @if (coat.CoatLaborCost > 0) { @coat.CoatLaborCost.ToString("C") labor }
}
} @* Prep service rows *@ @if (item.PrepServices != null && item.PrepServices.Any()) {
Prep Services Applied
@foreach (var ps in item.PrepServices) {
@ps.PrepServiceName @if (ps.EstimatedMinutes > 0) { (@ps.EstimatedMinutes min) } @if (laborRate > 0 && ps.EstimatedMinutes > 0) { @((ps.EstimatedMinutes / 60m * laborRate).ToString("C")) }
}
}
} else if (item.CatalogItemId.HasValue && !item.Coats.Any()) {
Catalog fixed price — no cost split available.
} else if (item.IsAiItem) {
AI-estimated price — cost breakdown not applicable.
}
} @* Totals footer *@ @if (hasItemCostData) {
All items — material / labor / equipment / total @pb.MaterialCosts.ToString("C") / @pb.LaborCosts.ToString("C") / @pb.EquipmentCosts.ToString("C") / @pb.ItemsSubtotal.ToString("C")
} @* Rates reference *@ @if (dbgCosts != null) {
Active rates used for this calculation
Labor rate @dbgCosts.StandardLaborRate.ToString("C2")/hr @if (dbgCosts.StandardLaborRate == 0) { Not Set }
Coating booth @dbgCosts.CoatingBoothCostPerHour.ToString("C2")/hr
Default powder cost @dbgCosts.PowderCoatingCostPerSqFt.ToString("C4")/sqft @if (dbgCosts.PowderCoatingCostPerSqFt == 0) { Not Set }
Markup @dbgCosts.GeneralMarkupPercentage.ToString("F1")% — applied to material costs
Additional coat charge @dbgCosts.AdditionalCoatLaborPercent.ToString("F0")% of first coat price per extra coat
Consumables surcharge 5% of raw powder cost
}
}
}
Actions
Edit Quote @{ var detHasEmail = !string.IsNullOrWhiteSpace(Model.CustomerEmail); var detHasMobile = !string.IsNullOrWhiteSpace(Model.CustomerMobilePhone); var detHasSmsConsent = Model.CustomerNotifyBySms && detHasMobile; var detProspectHasPhone = Model.IsProspect && !string.IsNullOrWhiteSpace(Model.ProspectPhone); var detProspectSmsReady = detProspectHasPhone && Model.ProspectSmsConsent; } @if (Model.StatusCode != "APPROVED" && Model.StatusCode != "CONVERTED") {
@Html.AntiForgeryToken() @if (detHasEmail) {
}
} @{ var detEmailOptedOut = detHasEmail && !Model.CustomerNotifyByEmail; } @if (detEmailOptedOut) { } else if (detHasEmail || !string.IsNullOrWhiteSpace(Model.ProspectEmail)) { } else { } @if (detHasMobile || detProspectSmsReady) { } @if (!detHasMobile && !detHasEmail && !detProspectHasPhone && string.IsNullOrWhiteSpace(Model.ProspectEmail)) {
No email or mobile number on file — update the customer record to send this quote electronically.
} @if (detHasMobile && !detHasSmsConsent) {
SMS consent required to send via text.
} @if (detProspectHasPhone && !Model.ProspectSmsConsent) {
SMS consent not recorded — edit the quote to enable SMS for this prospect.
} @if (!Model.ConvertedToJobId.HasValue) {
@Html.AntiForgeryToken()
} @if (Model.IsProspect && Model.StatusCode == "APPROVED") { Convert to Customer Only } @if (Model.ConvertedToJobId.HasValue) { View Job @Model.ConvertedToJobNumber } Download PDF Quote @if (Model.StatusCode != "CONVERTED") { Delete Quote }
@{ var quoteDeposits = ViewBag.Deposits as List ?? new List(); var quoteTotalDeposited = quoteDeposits.Sum(d => d.Amount); }
Deposits @if (quoteDeposits.Any()) { @quoteDeposits.Count }
@if (quoteTotalDeposited > 0) { Total: @quoteTotalDeposited.ToString("C") }
@if (!quoteDeposits.Any()) {

No deposits recorded

Click "Record Deposit" to add a customer deposit
} else {
@foreach (var dep in quoteDeposits) { }
Receipt # Date Method Reference Amount Status Actions
@dep.ReceiptNumber @dep.ReceivedDate.ToString("MM/dd/yyyy") @dep.PaymentMethod.ToString().Replace("CreditDebitCard","Credit/Debit Card").Replace("BankTransferACH","Bank Transfer/ACH").Replace("DigitalPayment","Digital") @dep.Reference @dep.Amount.ToString("C") @if (dep.AppliedToInvoiceId.HasValue) { Applied } else { Pending } @if (dep.AppliedToInvoiceId == null) { }
}
@{ var changeHistory = ViewBag.ChangeHistory as List; } @if (changeHistory != null && changeHistory.Any()) {
Revision History
@changeHistory.Count event@(changeHistory.Count != 1 ? "s" : "")
@{ // Group entries that were saved within 5 seconds of each other into one "revision" var groups = new List<(DateTime GroupTime, string ByName, List Entries)>(); foreach (var entry in changeHistory.OrderByDescending(c => c.ChangedAt)) { var last = groups.LastOrDefault(); if (last != default && (entry.ChangedAt - last.GroupTime).TotalSeconds <= 5 && last.ByName == entry.ChangedByName) last.Entries.Add(entry); else groups.Add((entry.ChangedAt, entry.ChangedByName ?? "", new List { entry })); } }
@foreach (var group in groups) { // Determine dominant event type for the group icon var isSent = group.Entries.Any(e => e.FieldName == "Sent"); var isApproved = group.Entries.Any(e => e.FieldName == "Status" && (e.NewValue?.Contains("Approved") == true)); var isDeclined = group.Entries.Any(e => e.FieldName == "Status" && (e.NewValue?.Contains("Rejected") == true || e.NewValue?.Contains("Declined") == true)); var isConverted = group.Entries.Any(e => e.FieldName == "Status" && e.NewValue?.Contains("Converted") == true); var hasTotalChange = group.Entries.Any(e => e.FieldName == "Total"); var (iconClass, iconBg, iconColor) = (isSent, isApproved, isDeclined, isConverted) switch { (true, _, _, _) => ("bi-envelope-check-fill", "#dbeafe", "#1d4ed8"), (_, true, _, _) => ("bi-check-circle-fill", "#dcfce7", "#15803d"), (_, _, true, _) => ("bi-x-circle-fill", "#fee2e2", "#dc2626"), (_, _, _, true) => ("bi-arrow-right-circle-fill", "#ede9fe", "#6d28d9"), _ => ("bi-pencil-fill", "#f3f4f6", "#6b7280"), };
@* Icon column *@
@* Content column *@
@(isSent ? "Sent to customer" : isApproved ? "Approved" : isDeclined ? "Rejected" : isConverted ? "Converted to job" : "Edited") — @group.GroupTime.ToLocalTime().ToString("MMM d, yyyy h:mm tt") @if (!string.IsNullOrWhiteSpace(group.ByName)) { by @group.ByName } @if (hasTotalChange) { var totalEntry = group.Entries.First(e => e.FieldName == "Total"); @totalEntry.OldValue → @totalEntry.NewValue }
@{ var detailEntries = group.Entries .Where(e => e.FieldName != "Sent") .ToList(); } @if (detailEntries.Any()) {
    @foreach (var e in detailEntries) {
  • @if (e.FieldName == "Total") { Total changed from @e.OldValue to @e.NewValue } else if (e.FieldName == "Status") { Status: @e.OldValue@e.NewValue } else if (e.FieldName == "Quote Items") { @e.ChangeDescription } else if (!string.IsNullOrWhiteSpace(e.OldValue) && !string.IsNullOrWhiteSpace(e.NewValue)) { @e.FieldName: @e.OldValue → @e.NewValue } else { @e.ChangeDescription }
  • }
} @if (isSent) { var sentEntry = group.Entries.First(e => e.FieldName == "Sent"); @sentEntry.NewValue }
}
}
@section Styles { } @section Scripts { }