Merge master into dev: quote stat cards Converted fix

This commit is contained in:
2026-06-15 16:29:59 -04:00
3 changed files with 28 additions and 7 deletions
@@ -33,10 +33,17 @@ public interface IQuoteRepository : IRepository<Quote>
/// <summary> /// <summary>
/// Returns aggregate stat counts and total value for the Index view stat cards, scoped to the /// Returns aggregate stat counts and total value for the Index view stat cards, scoped to the
/// current company by the global query filter. Pass status ID sets (derived from QuoteStatusLookup) /// given <paramref name="companyId"/> (explicit predicate for defense in depth, in addition to
/// to classify open vs. approved/converted quotes. /// the global query filter). Pass status ID sets (derived from QuoteStatusLookup) to classify
/// open vs. approved/converted quotes.
/// <para>
/// <paramref name="excludedStatusId"/> drops quotes in that status (Converted) from every card so
/// the stat strip reflects the same population as the default Index list, which hides Converted
/// quotes. Without this, a converted quote (which cannot be deleted while a job is linked) inflates
/// TotalValue and the approved count even though it never appears in the list.
/// </para>
/// </summary> /// </summary>
Task<QuoteIndexStats> GetIndexStatsAsync(List<int> openStatusIds, List<int> approvedConvertedStatusIds); Task<QuoteIndexStats> GetIndexStatsAsync(int companyId, List<int> openStatusIds, List<int> approvedConvertedStatusIds, int? excludedStatusId = null);
/// <summary> /// <summary>
/// Loads quote items with their coat passes (InventoryItem + Vendor) and prep services for /// Loads quote items with their coat passes (InventoryItem + Vendor) and prep services for
@@ -69,10 +69,21 @@ public class QuoteRepository : Repository<Quote>, IQuoteRepository
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<QuoteIndexStats> GetIndexStatsAsync(List<int> openStatusIds, List<int> approvedConvertedStatusIds) public async Task<QuoteIndexStats> GetIndexStatsAsync(int companyId, List<int> openStatusIds, List<int> approvedConvertedStatusIds, int? excludedStatusId = null)
{ {
var stats = await _context.Quotes // Explicit CompanyId predicate (defense in depth) on top of the global tenant filter.
.Where(q => !q.IsDeleted) var query = _context.Quotes
.Where(q => q.CompanyId == companyId && !q.IsDeleted);
// Exclude the same status the default Index list hides (Converted) so the stat strip and the
// visible list summarize the identical population — a converted quote left behind by a blocked
// delete must not inflate the cards while being invisible in the list.
if (excludedStatusId.HasValue)
{
query = query.Where(q => q.QuoteStatusId != excludedStatusId.Value);
}
var stats = await query
.Select(q => new { q.QuoteStatusId, q.Total }) .Select(q => new { q.QuoteStatusId, q.Total })
.ToListAsync(); .ToListAsync();
@@ -248,7 +248,10 @@ public class QuotesController : Controller
var approvedConvertedIds = quoteStatuses var approvedConvertedIds = quoteStatuses
.Where(s => s.StatusCode == AppConstants.StatusCodes.Quote.Approved || s.StatusCode == AppConstants.StatusCodes.Quote.Converted) .Where(s => s.StatusCode == AppConstants.StatusCodes.Quote.Approved || s.StatusCode == AppConstants.StatusCodes.Quote.Converted)
.Select(s => s.Id).ToList(); .Select(s => s.Id).ToList();
var indexStats = await _unitOfWork.Quotes.GetIndexStatsAsync(draftSentIds, approvedConvertedIds); // Exclude Converted from the stat cards so they match the default list (which hides Converted).
var convertedStatusId = quoteStatuses
.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Quote.Converted)?.Id;
var indexStats = await _unitOfWork.Quotes.GetIndexStatsAsync(companyId, draftSentIds, approvedConvertedIds, convertedStatusId);
ViewBag.StatOpenCount = indexStats.OpenCount; ViewBag.StatOpenCount = indexStats.OpenCount;
ViewBag.StatApprovedCount = indexStats.ApprovedConvertedCount; ViewBag.StatApprovedCount = indexStats.ApprovedConvertedCount;
ViewBag.StatTotalValue = indexStats.TotalValue; ViewBag.StatTotalValue = indexStats.TotalValue;