90bc0d965f
Migrated InvoicesController, QuotesController, JobsController, BillsController, PurchaseOrdersController, and CustomersController to route all data access through IUnitOfWork typed/generic repositories instead of injecting ApplicationDbContext directly. New typed repositories added: IJobRepository (GetScheduledJobsForDateAsync, GetActiveJobsForMobileAsync, LoadForCostingAsync), INotificationLogRepository (GetLatestForJobAsync, GetAllForJobAsync), IQuoteRepository (GetItemsWithCoatsAsync with CatalogItem eager load + AsNoTracking), and IJobRepository.GetOrphanedConversionJobAsync. All EF complex include chains relocated into repository methods; controllers now call named query methods rather than composing raw IQueryable chains. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
112 lines
4.2 KiB
C#
112 lines
4.2 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using PowderCoating.Core.Entities;
|
|
using PowderCoating.Core.Interfaces.Repositories;
|
|
using PowderCoating.Infrastructure.Data;
|
|
|
|
namespace PowderCoating.Infrastructure.Repositories;
|
|
|
|
/// <summary>
|
|
/// Typed repository for <see cref="Quote"/> that provides domain-specific queries previously
|
|
/// scattered as inline EF expressions across <c>QuotesController</c> and
|
|
/// <c>QuoteApprovalController</c>.
|
|
/// </summary>
|
|
public class QuoteRepository : Repository<Quote>, IQuoteRepository
|
|
{
|
|
public QuoteRepository(ApplicationDbContext context) : base(context) { }
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<Quote?> 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;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<Quote?> 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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<List<QuoteChangeHistory>> GetChangeHistoryAsync(int quoteId)
|
|
{
|
|
return await _context.QuoteChangeHistories
|
|
.Where(h => h.QuoteId == quoteId && !h.IsDeleted)
|
|
.Include(h => h.ChangedBy)
|
|
.OrderByDescending(h => h.ChangedAt)
|
|
.AsNoTracking()
|
|
.ToListAsync();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<QuoteIndexStats> GetIndexStatsAsync(List<int> openStatusIds, List<int> 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));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<List<QuoteItem>> 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();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<string?> 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();
|
|
}
|
|
}
|