using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Interfaces.Repositories;
using PowderCoating.Infrastructure.Data;
namespace PowderCoating.Infrastructure.Repositories;
///
/// Typed repository for that provides domain-specific queries previously
/// scattered as inline EF expressions across QuotesController and
/// QuoteApprovalController.
///
public class QuoteRepository : Repository, IQuoteRepository
{
public QuoteRepository(ApplicationDbContext context) : base(context) { }
///
public async Task LoadForDetailsAsync(int id)
{
var quote = await _context.Quotes
.Where(q => q.Id == id && !q.IsDeleted)
.Include(q => q.Customer)
.Include(q => q.PreparedBy)
.Include(q => q.QuoteStatus)
.Include(q => q.OvenCost)
.Include(q => q.QuotePrepServices.Where(qps => !qps.IsDeleted))
.ThenInclude(qps => qps.PrepService)
.FirstOrDefaultAsync();
if (quote == null) return null;
// QuoteItems with nested coats and prep services loaded separately to avoid
// cartesian explosion from multiple collection includes in a single query.
quote.QuoteItems = await _context.QuoteItems
.Where(qi => qi.QuoteId == id && !qi.IsDeleted)
.Include(qi => qi.Coats)
.ThenInclude(c => c.InventoryItem)
.Include(qi => qi.Coats)
.ThenInclude(c => c.Vendor)
.Include(qi => qi.CatalogItem)
.Include(qi => qi.PrepServices)
.ThenInclude(ps => ps.PrepService)
.ToListAsync();
return quote;
}
///
public async Task GetByApprovalTokenAsync(string token)
{
// IgnoreQueryFilters: approval portal is unauthenticated — no tenant context on the request.
return await _context.Quotes
.IgnoreQueryFilters()
.Include(q => q.Customer)
.Include(q => q.QuoteStatus)
.Include(q => q.QuoteItems.Where(qi => !qi.IsDeleted))
.FirstOrDefaultAsync(q => q.ApprovalToken == token && !q.IsDeleted);
}
///
public async Task> GetChangeHistoryAsync(int quoteId)
{
return await _context.QuoteChangeHistories
.Where(h => h.QuoteId == quoteId && !h.IsDeleted)
.Include(h => h.ChangedBy)
.OrderByDescending(h => h.ChangedAt)
.AsNoTracking()
.ToListAsync();
}
///
public async Task GetIndexStatsAsync(List openStatusIds, List approvedConvertedStatusIds)
{
var stats = await _context.Quotes
.Where(q => !q.IsDeleted)
.Select(q => new { q.QuoteStatusId, q.Total })
.ToListAsync();
return new QuoteIndexStats(
OpenCount: stats.Count(q => openStatusIds.Contains(q.QuoteStatusId)),
ApprovedConvertedCount: stats.Count(q => approvedConvertedStatusIds.Contains(q.QuoteStatusId)),
TotalValue: stats.Sum(q => q.Total));
}
///
public async Task> GetItemsWithCoatsAsync(int quoteId)
{
return await _context.QuoteItems
.Where(qi => qi.QuoteId == quoteId && !qi.IsDeleted)
.Include(qi => qi.Coats)
.ThenInclude(c => c.InventoryItem)
.Include(qi => qi.Coats)
.ThenInclude(c => c.Vendor)
.Include(qi => qi.CatalogItem)
.Include(qi => qi.PrepServices)
.ThenInclude(ps => ps.PrepService)
.AsNoTracking()
.ToListAsync();
}
///
public async Task GetLastQuoteNumberByPrefixAsync(int companyId, string prefix)
{
return await _context.Quotes
.IgnoreQueryFilters()
.Where(q => q.CompanyId == companyId && q.QuoteNumber.StartsWith(prefix))
.OrderByDescending(q => q.QuoteNumber)
.Select(q => q.QuoteNumber)
.FirstOrDefaultAsync();
}
}