Add CRM features: Outstanding Pickups, Customer Notes, Clone Job, Preferred Powders

- Outstanding Pickups card on Customer Details shows jobs awaiting pickup with age badges
- Customer Notes log: inline add/delete notes with important flag, AJAX-backed
- Clone Job action on Jobs controller; Repeat Last Job button on Customer Details quick actions
- Preferred Powders per customer: typeahead inventory search, AJAX add/remove
- CustomerPreferredPowder entity + migration; unit tests for CRM stats/timeline logic
- Fix EF Core concurrency bug: parallel Task.WhenAll FindAsync replaced with sequential awaits

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 19:59:32 -04:00
parent 7cbae31916
commit 711cd01cd3
14 changed files with 12725 additions and 22 deletions
@@ -0,0 +1,35 @@
namespace PowderCoating.Application.DTOs.Customer;
/// <summary>A single entry in the customer activity timeline feed on the Details page.</summary>
public class CustomerTimelineEventDto
{
public DateTime Date { get; set; }
public string Icon { get; set; } = string.Empty;
public string BadgeColor { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? Subtitle { get; set; }
public decimal? Amount { get; set; }
public int? EntityId { get; set; }
public string? LinkController { get; set; }
public string? LinkAction { get; set; }
}
/// <summary>Aggregate lifetime metrics displayed in the CRM stats card on Customer Details.</summary>
public class CustomerLifetimeStatsDto
{
public int TotalJobs { get; set; }
public int ActiveJobs { get; set; }
/// <summary>Sum of Total on non-voided invoices.</summary>
public decimal TotalRevenue { get; set; }
/// <summary>Sum of AmountPaid on non-voided invoices.</summary>
public decimal TotalCollected { get; set; }
/// <summary>Mean FinalPrice across all jobs for this customer.</summary>
public decimal AverageJobValue { get; set; }
public DateTime? LastJobDate { get; set; }
public int? DaysSinceLastJob { get; set; }
public int TotalQuotes { get; set; }
public int TotalInvoices { get; set; }
public decimal OpenBalance { get; set; }
/// <summary>Id of the most recent job — used by the "Repeat Last Job" button on Customer Details.</summary>
public int? LastJobId { get; set; }
}