ed35362c7a
- 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>
62 lines
2.4 KiB
C#
62 lines
2.4 KiB
C#
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; }
|
||
}
|