@model PowderCoating.Application.DTOs.Job.JobDto @{ ViewData["Title"] = $"Job {Model.JobNumber}"; ViewData["PageIcon"] = "bi-briefcase"; var guidedActivationCallout = ViewBag.GuidedActivationCallout as PowderCoating.Web.ViewModels.GuidedActivation.GuidedActivationCalloutViewModel; bool quoteUpdated = ViewBag.QuoteUpdatedAfterConversion == true; }
@if (Model.IsReworkJob && Model.OriginalJobId.HasValue) {
Rework Job — This job was created to redo work from @(Model.OriginalJobNumber ?? $"Job #{Model.OriginalJobId}") . All costs for this redo are tracked here separately from the original job.
}
Status: @Model.StatusDisplayName @if (Model.RequiresCustomerApproval && !Model.IsCustomerApproved) { Awaiting Customer Approval }
@if (quoteUpdated) { bool canResync = ViewBag.CanResyncFromQuote == true;
Source quote was updated on @(((DateTime)ViewBag.QuoteUpdatedAt).ToString("MMM d, yyyy"))
@ViewBag.SourceQuoteNumber was edited after this job was created. @if (canResync) { Re-sync to replace job items and pricing with the latest quote, or dismiss if you've already handled it manually. } else { Shop work has started — review the quote and apply any changes manually. }
@if (canResync) {
@Html.AntiForgeryToken()
}
@Html.AntiForgeryToken()
} @if (guidedActivationCallout?.Show == true) {
@guidedActivationCallout.Title
@guidedActivationCallout.Message
@if (!string.IsNullOrWhiteSpace(guidedActivationCallout.ActionText)) { @guidedActivationCallout.ActionText } @if (!string.IsNullOrWhiteSpace(guidedActivationCallout.SecondaryActionText)) { @guidedActivationCallout.SecondaryActionText }
}
Job Information

@Model.JobNumber

@Html.AntiForgeryToken()

@Model.PriorityDisplayName

@(Model.CustomerPO ?? "Not provided")

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

@Model.ProjectName

}

@Model.Description

@* Preparation Services *@ @if (Model.PrepServices != null && Model.PrepServices.Any()) {

@foreach (var service in Model.PrepServices) { @service.ServiceName }
}
Schedule
@(Model.ScheduledDate.HasValue ? Model.ScheduledDate.Value.ToString("MMMM dd, yyyy") : "Not scheduled")
Saving…
@{ var isOverdue = Model.DueDate.HasValue && Model.DueDate.Value < DateTime.Now && Model.StatusCode != "COMPLETED" && Model.StatusCode != "READYFORPICKUP" && Model.StatusCode != "DELIVERED"; }
@if (Model.DueDate.HasValue) { @Model.DueDate.Value.ToString("MMMM dd, yyyy") if (isOverdue) { Overdue } } else { Not set }
Saving…
Saving…
Job Items @if (Model.Items.Any()) { @Model.Items.Count }
@if (Model.Items.Any(i => i.Description != null && i.Description.StartsWith("Custom Powder Order"))) { } @if (Model.Items.Any()) { var allItems = Model.Items.ToList(); var catalogItems = Model.Items.Where(i => i.CatalogItemId.HasValue).ToList(); var laborItems = Model.Items.Where(i => !i.CatalogItemId.HasValue && i.IsLaborItem).ToList(); var customItems = Model.Items.Where(i => !i.CatalogItemId.HasValue && !i.IsLaborItem).ToList();
@* -- Catalog Products -- *@ @if (catalogItems.Any()) {
Catalog Products
@foreach (var item in catalogItems) { var catIdx = allItems.IndexOf(item); }
Product Qty Unit Price Total
@item.Description @if (item.IsAiItem) { AI } @if (item.IsCustomFormulaItem) { Formula } @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 (!string.IsNullOrEmpty(coat.Finish)) { · @coat.Finish } @if (coat.PowderToOrder.HasValue && coat.PowderToOrder.Value > 0) { @coat.PowderToOrder.Value.ToString("0.##") lbs @if (!coat.InventoryItemId.HasValue) { ORDER POWDER } } @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)) {
@item.Notes }
@item.Quantity @item.UnitPrice.ToString("C") @item.TotalPrice.ToString("C")
} @* -- Custom Work -- *@ @if (customItems.Any()) {
Custom Work
@foreach (var item in customItems) { var custIdx = allItems.IndexOf(item); // Use stored PowderToOrder per coat; fall back to calculating from efficiency data // Note: row has data-item-id for inline editing decimal totalPowderNeeded = 0; if (item.Coats != null && item.Coats.Any(c => c.PowderToOrder > 0)) { totalPowderNeeded = item.Coats.Sum(c => c.PowderToOrder ?? 0); } else if (item.Coats != null && item.Coats.Any() && item.SurfaceAreaSqFt > 0) { foreach (var c in item.Coats) { var cov = c.CoverageSqFtPerLb > 0 ? c.CoverageSqFtPerLb : 30m; var eff = c.TransferEfficiency > 0 ? c.TransferEfficiency / 100m : 0.65m; totalPowderNeeded += (item.SurfaceAreaSqFt * item.Quantity) / (cov * eff); } } else if (item.SurfaceAreaSqFt > 0) { totalPowderNeeded = (item.SurfaceAreaSqFt * item.Quantity) / (30m * 0.65m); } }
Description Qty Surface Area Est. Minutes Coating Needed Unit Price Total
@item.Description @if (item.RequiresSandblasting || item.RequiresMasking) {
@if (item.RequiresSandblasting) { Sandblast } @if (item.RequiresMasking) { Mask } } @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 (!string.IsNullOrEmpty(coat.Finish)) { · @coat.Finish } @if (coat.PowderToOrder.HasValue && coat.PowderToOrder.Value > 0) { @coat.PowderToOrder.Value.ToString("0.##") lbs @if (!coat.InventoryItemId.HasValue) { ORDER POWDER } } @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)) {
@item.Notes }
@item.Quantity @if (item.SurfaceAreaSqFt > 0) { @item.SurfaceAreaSqFt.ToString("F2") @ViewBag.AreaUnit
per item } else { }
@if (item.EstimatedMinutes > 0) { @item.EstimatedMinutes min
per item } else { }
@if (totalPowderNeeded > 0) { @totalPowderNeeded.ToString("F2") lbs
total batch } else { }
@item.UnitPrice.ToString("C") @item.TotalPrice.ToString("C")
} @* -- Labor -- *@ @if (laborItems.Any()) {
Labor
@foreach (var item in laborItems) { var labIdx = allItems.IndexOf(item); }
Description Qty Est. Time Unit Price Total
@item.Description @if (!string.IsNullOrEmpty(item.Notes)) {
@item.Notes }
@item.Quantity @if (item.EstimatedMinutes > 0) { @item.EstimatedMinutes min } else { } @item.UnitPrice.ToString("C") @item.TotalPrice.ToString("C")
} @* -- Mobile cards -- *@
@foreach (var item in Model.Items) { var mobileIdx = allItems.IndexOf(item);
@item.Description
Qty: @item.Quantity
@if (item.SurfaceAreaSqFt > 0) {
Surface Area @item.SurfaceAreaSqFt.ToString("F2") per item
} @if (item.EstimatedMinutes > 0) {
Est. Time @item.EstimatedMinutes min
} @if (item.Coats != null && item.Coats.Any()) {
Coats @foreach (var coat in item.Coats.OrderBy(c => c.Sequence)) { @coat.CoatName@if (!string.IsNullOrEmpty(coat.ColorName)) { – @coat.ColorName } @if (!string.IsNullOrEmpty(coat.Notes)) {
@coat.Notes
}
}
}
Unit Price @item.UnitPrice.ToString("C")
Total @item.TotalPrice.ToString("C")
}
} else {

No items on this job yet.

}
Total: @{ var estimatedMins = Model.Items?.Sum(i => i.EstimatedMinutes * i.Quantity) ?? 0; var estimatedHrs = estimatedMins / 60m; } @if (estimatedHrs > 0) { Est: @estimatedHrs.ToString("0.##") hrs }
No time logged yet.
Worker Date Hours Stage Notes
Total
@if (!Model.StatusIsTerminal) { }
@if (Model.IntakeDate.HasValue) {
Checked In @Model.IntakeDate.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM d, yyyy h:mm tt")
@if (!string.IsNullOrEmpty(Model.IntakeCheckedByName)) {
By @Model.IntakeCheckedByName
} @if (Model.IntakePartCount.HasValue) {
Part Count @Model.IntakePartCount.Value
} @if (!string.IsNullOrEmpty(Model.IntakeConditionNotes)) {
Condition Notes

@Model.IntakeConditionNotes

}
} else {

Parts not yet checked in. @if (!Model.StatusIsTerminal) { Record intake }

}
@if (!string.IsNullOrEmpty(Model.SpecialInstructions)) {
Special Instructions

@Model.SpecialInstructions

} @if (!string.IsNullOrEmpty(Model.InternalNotes)) {
Internal Notes Staff Only

@Model.InternalNotes

} @if (!string.IsNullOrWhiteSpace(Model.Tags)) {
Tags
@foreach (var tag in Model.Tags.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()).Where(t => !string.IsNullOrWhiteSpace(t))) { @tag }
} @{ bool canUploadJobPhoto = ViewBag.CanUploadJobPhoto is bool jobPhotoOk && jobPhotoOk; int jobPhotoUsed = ViewBag.JobPhotoUsed is int jpu ? jpu : 0; int jobPhotoMax = ViewBag.JobPhotoMax is int jpm ? jpm : -1; }
@if (canUploadJobPhoto) { } else { }

No photos uploaded yet

Click "Upload Photo" to add photos
@{ var jobDeposits = ViewBag.Deposits as List ?? new List(); var totalDeposited = jobDeposits.Sum(d => d.Amount); }
@if (totalDeposited > 0) { Total: @totalDeposited.ToString("C") }
@if (!jobDeposits.Any()) {

No deposits recorded

Click "Record Deposit" to add a customer deposit
} else {
@foreach (var dep in jobDeposits) { }
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 materialsUsed = ViewBag.MaterialsUsed as List ?? new List(); }
@if (!materialsUsed.Any()) {
No materials have been logged for this job yet. Click Log Material above or scan the QR label on an inventory item.
} else {
@foreach (var mat in materialsUsed) { }
Item Qty Used Reason Date Notes
@(mat.InventoryItem?.Name ?? "Unknown Item") @(Math.Abs(mat.Quantity).ToString("N2")) @mat.InventoryItem?.UnitOfMeasure @{ var reasonBadge = mat.TransactionType switch { PowderCoating.Core.Enums.InventoryTransactionType.JobUsage => ("bg-primary", "Job Usage"), PowderCoating.Core.Enums.InventoryTransactionType.Waste => ("bg-warning text-dark", "Waste / Spillage"), PowderCoating.Core.Enums.InventoryTransactionType.Adjustment => ("bg-secondary", "Correction"), PowderCoating.Core.Enums.InventoryTransactionType.Transfer => ("bg-info text-dark", "Transfer Out"), _ => ("bg-secondary", mat.TransactionType.ToString()) }; } @reasonBadge.Item2 @mat.TransactionDate.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM d, yyyy h:mm tt") @mat.Notes
Total @materialsUsed.Sum(m => Math.Abs(m.Quantity)).ToString("N2") units
}
@{ var intakeExpectedCount = Model.Items?.Sum(i => (int)i.Quantity) ?? 0; var showAdvanceToggle = !Model.StatusIsTerminal && Model.StatusCode != "IN_PREPARATION"; } @if (Model.CompletedDate.HasValue) {
Job Completion Details Completed

@Model.CompletedDate.Value.ToString("MMM dd, yyyy h:mm tt")

@if (Model.ActualTimeSpentHours.HasValue) {

@Model.ActualTimeSpentHours.Value.ToString("0.##") hours

}
@if (Model.Items != null && Model.Items.Any(i => i.Coats != null && i.Coats.Any(c => c.ActualPowderUsedLbs.HasValue))) {
Actual Powder Usage
@foreach (var item in Model.Items) { @if (item.Coats != null && item.Coats.Any()) { @foreach (var coat in item.Coats.OrderBy(c => c.Sequence)) { @if (coat.ActualPowderUsedLbs.HasValue) { var variance = coat.ActualPowderUsedLbs.Value - (coat.PowderToOrder ?? 0); var varianceClass = variance > 0 ? "text-danger" : variance < 0 ? "text-success" : "text-muted"; } } } }
Item Coat Color Estimated (lbs) Actual (lbs) Variance
@item.Description @coat.CoatName @if (!string.IsNullOrEmpty(coat.ColorName)) { @coat.ColorName @if (!string.IsNullOrEmpty(coat.ColorCode)) { (@coat.ColorCode) } } @((coat.PowderToOrder ?? 0).ToString("0.##")) @coat.ActualPowderUsedLbs.Value.ToString("0.##") @(variance > 0 ? "+" : "")@variance.ToString("0.##")
}
}
Actions
Edit Job @if (!Model.StatusIsTerminal) { @Html.Raw(Model.IntakeDate.HasValue ? "Intake ✓" : "Intake") } @{ var panelInvoiceId = ViewBag.JobInvoiceId as int?; var voidedInvoices = ViewBag.JobVoidedInvoices as IEnumerable ?? []; } @if (panelInvoiceId.HasValue) { View Invoice } else { Create Invoice } @foreach (var vi in voidedInvoices) { @vi.InvoiceNumber (Voided) } Print Work Order @if (Model.QuoteId.HasValue) { View Original Quote } @if (Model.StatusCode != "COMPLETED" && Model.StatusCode != "CANCELLED") { } @if ((bool)(ViewBag.IsAdminOrManager ?? false) && (bool)(ViewBag.SmsEnabled ?? false) && Model.CustomerNotifyBySms && !string.IsNullOrWhiteSpace(Model.CustomerMobilePhone)) { } Delete Job
@{ var jobPb = ViewBag.JobPricingBreakdown as PowderCoating.Application.DTOs.Quote.QuotePricingBreakdownDto; }
Pricing Summary
@if (!string.IsNullOrWhiteSpace(Model.OvenLabel)) {
Oven @Model.OvenLabel
} @if (jobPb != null) {
Items Subtotal: @jobPb.ItemsSubtotal.ToString("C")
@if (jobPb.OvenBatchCost > 0) {
Oven (@jobPb.OvenBatches batch@(jobPb.OvenBatches != 1 ? "es" : "")@(jobPb.OvenCycleMinutes > 0 ? $" × {jobPb.OvenCycleMinutes} min" : "")): @jobPb.OvenBatchCost.ToString("C")
} @if (jobPb.FacilityOverheadCost > 0) {
Facility Overhead (@jobPb.FacilityOverheadRatePerHour.ToString("C2")/hr): @jobPb.FacilityOverheadCost.ToString("C")
} @if (jobPb.ShopSuppliesAmount > 0) {
Shop Supplies (@jobPb.ShopSuppliesPercent%): @jobPb.ShopSuppliesAmount.ToString("C")
}
Subtotal: @jobPb.SubtotalBeforeDiscount.ToString("C")
@if (jobPb.DiscountAmount > 0) {
@if (Model.DiscountType == "Percentage") { Discount (@Model.DiscountValue% Off): } else if (Model.DiscountType == "FixedAmount") { Discount (@Model.DiscountValue.ToString("C") Off): } else { Discount (@jobPb.DiscountPercent.ToString("F1")%): } -@jobPb.DiscountAmount.ToString("C")
@if (!string.IsNullOrWhiteSpace(Model.DiscountReason)) {
Reason: @Model.DiscountReason
} } @if (Model.IsRushJob && jobPb.RushFee > 0) {
Rush Job Fee: @jobPb.RushFee.ToString("C")
} @if (jobPb.TaxAmount > 0) {
Tax (@jobPb.TaxPercent.ToString("G29")%): @jobPb.TaxAmount.ToString("C")
}
Total:
@jobPb.Total.ToString("C")
@* Collapsible detail breakdown *@
@{ var directCosts = jobPb.MaterialCosts + jobPb.LaborCosts + jobPb.EquipmentCosts; var hasCostBreakdown = directCosts > 0; var allCatalog = Model.Items != null && Model.Items.All(i => i.CatalogItemId.HasValue); } @* Section 1: Item Costs *@
Item Costs
@if (hasCostBreakdown) {
Material (powder + consumables) @jobPb.MaterialCosts.ToString("C")
Labor @jobPb.LaborCosts.ToString("C")
Equipment (oven + booth) @jobPb.EquipmentCosts.ToString("C")
Direct costs @directCosts.ToString("C")
Markup (@jobPb.ProfitPercent.ToString("F0")% baked into item prices) @((jobPb.ItemsSubtotal - directCosts).ToString("C"))
} else if (allCatalog) {
All items use fixed catalog pricing — no per-category cost split available.
} else {
Cost breakdown not available.
}
Items subtotal @jobPb.ItemsSubtotal.ToString("C")
@* Section 2: Job-Level Additions *@ @if (jobPb.OvenBatchCost > 0 || jobPb.FacilityOverheadCost > 0 || jobPb.ShopSuppliesAmount > 0 || jobPb.OverheadCosts > 0) {
Job-Level Additions
@if (jobPb.OvenBatchCost > 0) {
Oven batch (@jobPb.OvenBatches batch@(jobPb.OvenBatches != 1 ? "es" : "")@(jobPb.OvenCycleMinutes > 0 ? $", {jobPb.OvenCycleMinutes} min/cycle" : "")) @jobPb.OvenBatchCost.ToString("C")
} @if (jobPb.FacilityOverheadCost > 0) {
Facility overhead (@jobPb.FacilityOverheadRatePerHour.ToString("C2")/hr × estimated hours) @jobPb.FacilityOverheadCost.ToString("C")
} @if (jobPb.ShopSuppliesAmount > 0) {
Shop supplies (@jobPb.ShopSuppliesPercent.ToString("F1")%) @jobPb.ShopSuppliesAmount.ToString("C")
} @if (jobPb.OverheadCosts > 0) {
Overhead (@jobPb.OverheadPercent.ToString("F1")%) @jobPb.OverheadCosts.ToString("C")
}
} @* Section 3: Final Calculation *@
Final Calculation
Subtotal @jobPb.SubtotalBeforeDiscount.ToString("C")
@if (jobPb.DiscountAmount > 0) {
Discount (@jobPb.DiscountPercent.ToString("F1")%) -@jobPb.DiscountAmount.ToString("C")
After discount @jobPb.SubtotalAfterDiscount.ToString("C")
} @if (jobPb.RushFee > 0) {
Rush fee @jobPb.RushFee.ToString("C")
} @if (jobPb.TaxAmount > 0) {
Tax (@jobPb.TaxPercent.ToString("G29")%) @jobPb.TaxAmount.ToString("C")
}
Total @jobPb.Total.ToString("C")
@{ var jobTotalDirectCost = jobPb.MaterialCosts + jobPb.LaborCosts + jobPb.EquipmentCosts + jobPb.OvenBatchCost + jobPb.FacilityOverheadCost + jobPb.ShopSuppliesAmount; var jobGrossProfit = jobPb.Total - jobTotalDirectCost; var jobEffectiveMargin = jobPb.Total > 0 ? (jobGrossProfit / jobPb.Total * 100m) : 0m; } @if (jobTotalDirectCost > 0) {
Effective gross margin @jobEffectiveMargin.ToString("F1")%
}
} else { @* Fallback: no items yet *@ @if (Model.QuoteId.HasValue) {

@Model.QuotedPrice.ToString("C")

}

@Model.FinalPrice.ToString("C")

}
Timeline

@Model.CreatedAt.ToString("MMMM dd, yyyy")

@if (Model.RequiresCustomerApproval) {

@if (Model.IsCustomerApproved) { Approved } else { Pending }

}
Rework / Warranty 0
Job Costing
Loading...
@if (ViewBag.ChangeHistory != null && ((List)ViewBag.ChangeHistory).Any()) {
Change History
@foreach (var change in (List)ViewBag.ChangeHistory) { }
Date & Time Changed By Field Old Value New Value
@change.ChangedAt.ToString("MM/dd/yyyy")
@change.ChangedAt.ToString("h:mm tt")
@change.ChangedByName @change.FieldName @if (!string.IsNullOrEmpty(change.OldValue)) { @change.OldValue } else { None } @if (!string.IsNullOrEmpty(change.NewValue)) { @change.NewValue } else { None }
}
@await Html.PartialAsync("_CompleteJobModal", Model) @if ((bool)(ViewBag.IsAdminOrManager ?? false) && (bool)(ViewBag.SmsEnabled ?? false)) { } @await Html.PartialAsync("_SqFtCalculatorModal") @await Html.PartialAsync("_ItemWizardModal") @if (ViewBag.InventoryCoatings != null) { } @if (ViewBag.CatalogItems != null) { } @section Styles { } @section Scripts { @if ((bool)(ViewBag.IsAdminOrManager ?? false) && (bool)(ViewBag.SmsEnabled ?? false)) { } }