diff --git a/src/PowderCoating.Core/Interfaces/Repositories/IQuoteRepository.cs b/src/PowderCoating.Core/Interfaces/Repositories/IQuoteRepository.cs index f4a0655..3a4192e 100644 --- a/src/PowderCoating.Core/Interfaces/Repositories/IQuoteRepository.cs +++ b/src/PowderCoating.Core/Interfaces/Repositories/IQuoteRepository.cs @@ -33,10 +33,17 @@ public interface IQuoteRepository : IRepository /// /// 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 (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. + /// + /// 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. + /// /// - Task GetIndexStatsAsync(List openStatusIds, List approvedConvertedStatusIds); + Task GetIndexStatsAsync(int companyId, List openStatusIds, List approvedConvertedStatusIds, int? excludedStatusId = null); /// /// Loads quote items with their coat passes (InventoryItem + Vendor) and prep services for diff --git a/src/PowderCoating.Infrastructure/Repositories/QuoteRepository.cs b/src/PowderCoating.Infrastructure/Repositories/QuoteRepository.cs index 25e0d86..b9d9ebe 100644 --- a/src/PowderCoating.Infrastructure/Repositories/QuoteRepository.cs +++ b/src/PowderCoating.Infrastructure/Repositories/QuoteRepository.cs @@ -69,10 +69,21 @@ public class QuoteRepository : Repository, IQuoteRepository } /// - public async Task GetIndexStatsAsync(List openStatusIds, List approvedConvertedStatusIds) + public async Task GetIndexStatsAsync(int companyId, List openStatusIds, List 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(); diff --git a/src/PowderCoating.Web/Controllers/QuotesController.cs b/src/PowderCoating.Web/Controllers/QuotesController.cs index a58604f..63189f2 100644 --- a/src/PowderCoating.Web/Controllers/QuotesController.cs +++ b/src/PowderCoating.Web/Controllers/QuotesController.cs @@ -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;