Phase 2: Eliminate ApplicationDbContext from domain controllers

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>
This commit is contained in:
2026-04-27 21:20:39 -04:00
parent 80b0e547cc
commit 90bc0d965f
20 changed files with 730 additions and 878 deletions
@@ -103,4 +103,76 @@ public class JobRepository : Repository<Job>, IJobRepository
.AsNoTracking()
.ToListAsync();
}
/// <inheritdoc/>
public async Task<string?> GetLastJobNumberByPrefixAsync(int companyId, string prefix)
{
return await _context.Jobs
.IgnoreQueryFilters()
.Where(j => j.CompanyId == companyId && j.JobNumber.StartsWith(prefix))
.OrderByDescending(j => j.JobNumber)
.Select(j => j.JobNumber)
.FirstOrDefaultAsync();
}
/// <inheritdoc/>
public async Task<Job?> GetOrphanedConversionJobAsync(int quoteId, int companyId)
{
return await _context.Jobs
.IgnoreQueryFilters()
.FirstOrDefaultAsync(j => j.QuoteId == quoteId && j.CompanyId == companyId);
}
/// <inheritdoc/>
public async Task<List<Job>> GetScheduledJobsForDateAsync(DateTime date, string? userId = null)
{
var query = _context.Jobs
.Include(j => j.Customer)
.Include(j => j.JobStatus)
.Include(j => j.JobPriority)
.Include(j => j.AssignedUser)
.Include(j => j.JobItems.Where(i => !i.IsDeleted))
.ThenInclude(i => i.Coats)
.Where(j => j.ScheduledDate.HasValue && j.ScheduledDate.Value.Date == date.Date
&& !j.IsDeleted && !j.JobStatus.IsTerminalStatus);
if (!string.IsNullOrEmpty(userId))
query = query.Where(j => j.AssignedUserId == userId);
return await query.ToListAsync();
}
/// <inheritdoc/>
public async Task<List<Job>> GetActiveJobsForMobileAsync(int companyId, string? workerId = null)
{
var query = _context.Jobs
.Include(j => j.Customer)
.Include(j => j.JobStatus)
.Include(j => j.JobPriority)
.Include(j => j.AssignedUser)
.Include(j => j.JobItems.Where(i => !i.IsDeleted))
.ThenInclude(i => i.Coats)
.Where(j => j.CompanyId == companyId && !j.IsDeleted && !j.JobStatus.IsTerminalStatus
&& j.JobStatus.StatusCode != "ON_HOLD" && j.JobStatus.StatusCode != "CANCELLED");
if (!string.IsNullOrEmpty(workerId))
query = query.Where(j => j.AssignedUserId == workerId);
return await query.ToListAsync();
}
/// <inheritdoc/>
public async Task<Job?> LoadForCostingAsync(int jobId, int companyId)
{
return await _context.Jobs
.Where(j => j.Id == jobId && j.CompanyId == companyId && !j.IsDeleted)
.Include(j => j.OvenCost)
.Include(j => j.Invoice)
.Include(j => j.JobItems.Where(i => !i.IsDeleted))
.ThenInclude(i => i.Coats.Where(c => !c.IsDeleted))
.Include(j => j.TimeEntries.Where(t => !t.IsDeleted))
.ThenInclude(t => t.Worker)
.AsNoTracking()
.FirstOrDefaultAsync();
}
}