namespace PowderCoating.Web.ViewModels.Reports; public class JobProfitabilityViewModel : ReportViewModelBase { public bool TimeTrackedOnly { get; set; } // ── Summary KPIs ────────────────────────────────────────────────────── public int TotalJobs { get; set; } public int JobsWithTimeEntries { get; set; } public decimal TotalRevenue { get; set; } public decimal TotalCollected { get; set; } public decimal TotalEstimatedCost { get; set; } public decimal TotalActualHours { get; set; } /// Average margin % across jobs that have at least some cost data. public decimal AvgMarginPercent { get; set; } // ── Detail rows ─────────────────────────────────────────────────────── public List Items { get; set; } = new(); } public class JobProfitabilityItem { public int JobId { get; set; } public string JobNumber { get; set; } = string.Empty; public string CustomerName { get; set; } = string.Empty; public string StatusName { get; set; } = string.Empty; public string StatusColorClass { get; set; } = "bg-secondary"; public DateTime JobDate { get; set; } /// The invoiced / final price of the job. public decimal FinalPrice { get; set; } /// How much has actually been collected on the linked invoice. public decimal AmountCollected { get; set; } /// Total hours logged via time entries. public decimal ActualHours { get; set; } /// ActualHours × StandardLaborRate. public decimal ActualLaborCost { get; set; } /// /// Sum of (ActualPowderUsedLbs ?? PowderToOrder) × PowderCostPerLb across all coats. /// Zero when no coat has pricing data. /// public decimal ActualPowderCost { get; set; } public decimal EstimatedTotalCost => ActualLaborCost + ActualPowderCost; public decimal GrossMargin => FinalPrice - EstimatedTotalCost; public decimal MarginPercent => FinalPrice > 0 ? Math.Round(GrossMargin / FinalPrice * 100, 1) : 0; /// True when at least one JobTimeEntry exists for this job. public bool HasTimeEntries { get; set; } }