Files
PowderCoatingLogix/src/PowderCoating.Web/ViewModels/Reports/JobProfitabilityViewModel.cs
T
spouliot ed35362c7a Add Formula Library ratings, Job Profitability report, and Quote Revision History improvements
- Formula Library ratings: thumbs up/down per company per formula; toggle on/off; sorts by net score; own formulas not rateable; FormulaLibraryRating entity + migration AddFormulaLibraryRatings
- Job Profitability report: actual labor cost (logged hours x StandardLaborRate) vs powder cost vs billed price per job; gross margin % color-coded; time-tracked-only filter; totals footer
- Quote Revision History: track Total price changes on every save; log Sent/Resent events with recipient email; replace flat table with grouped timeline UI (icons per event type, total-change badge on header)
- Setup Wizard: cap CompletedCount at TotalSteps so old 10-step data no longer shows 10/5
- Formula Library card: fix badge overflow on long titles; add Rate: label to make voting buttons discoverable
- Help docs and AI knowledge base updated for all three features

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 09:02:07 -04:00

62 lines
2.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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; }
/// <summary>Average margin % across jobs that have at least some cost data.</summary>
public decimal AvgMarginPercent { get; set; }
// ── Detail rows ───────────────────────────────────────────────────────
public List<JobProfitabilityItem> 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; }
/// <summary>The invoiced / final price of the job.</summary>
public decimal FinalPrice { get; set; }
/// <summary>How much has actually been collected on the linked invoice.</summary>
public decimal AmountCollected { get; set; }
/// <summary>Total hours logged via time entries.</summary>
public decimal ActualHours { get; set; }
/// <summary>ActualHours × StandardLaborRate.</summary>
public decimal ActualLaborCost { get; set; }
/// <summary>
/// Sum of (ActualPowderUsedLbs ?? PowderToOrder) × PowderCostPerLb across all coats.
/// Zero when no coat has pricing data.
/// </summary>
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;
/// <summary>True when at least one JobTimeEntry exists for this job.</summary>
public bool HasTimeEntries { get; set; }
}