Fix Quotes Index stat cards counting Converted quotes hidden from the list
The Quotes Index stat strip (OPEN / APPROVED / TOTAL VALUE) summed every non-deleted quote, while the default list hides Converted quotes. A quote converted to a job (whose deletion is blocked by the linked job) therefore stayed invisible in the list but kept inflating the cards -- e.g. a blank list showing "1" and a non-zero total value. GetIndexStatsAsync now excludes the Converted status so the cards reflect the same population as the default list. Converted value is intentionally dropped from the quote pipeline because it carries forward on the job (counting it in both would double-count the same dollars). Also adds an explicit CompanyId predicate to GetIndexStatsAsync (defense in depth) -- it was the only Quote query in the typed repo relying solely on the global tenant filter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -33,10 +33,17 @@ public interface IQuoteRepository : IRepository<Quote>
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// to classify open vs. approved/converted quotes.
|
||||
/// given <paramref name="companyId"/> (explicit predicate for defense in depth, in addition to
|
||||
/// 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>
|
||||
Task<QuoteIndexStats> GetIndexStatsAsync(List<int> openStatusIds, List<int> approvedConvertedStatusIds);
|
||||
Task<QuoteIndexStats> GetIndexStatsAsync(int companyId, List<int> openStatusIds, List<int> approvedConvertedStatusIds, int? excludedStatusId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Loads quote items with their coat passes (InventoryItem + Vendor) and prep services for
|
||||
|
||||
@@ -69,10 +69,21 @@ public class QuoteRepository : Repository<Quote>, IQuoteRepository
|
||||
}
|
||||
|
||||
/// <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
|
||||
.Where(q => !q.IsDeleted)
|
||||
// Explicit CompanyId predicate (defense in depth) on top of the global tenant filter.
|
||||
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 })
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
@@ -248,7 +248,10 @@ public class QuotesController : Controller
|
||||
var approvedConvertedIds = quoteStatuses
|
||||
.Where(s => s.StatusCode == AppConstants.StatusCodes.Quote.Approved || s.StatusCode == AppConstants.StatusCodes.Quote.Converted)
|
||||
.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.StatApprovedCount = indexStats.ApprovedConvertedCount;
|
||||
ViewBag.StatTotalValue = indexStats.TotalValue;
|
||||
|
||||
Reference in New Issue
Block a user