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>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
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; }
|
||||
}
|
||||
Reference in New Issue
Block a user