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 multi-level /// include queries that the generic cannot express. /// The base class handles all standard CRUD operations; this class adds read queries /// that were previously scattered as inline EF expressions inside controllers. /// public class JobRepository : Repository, IJobRepository { public JobRepository(ApplicationDbContext context) : base(context) { } /// public async Task> GetBoardJobsAsync() { return await _context.Jobs .AsNoTracking() .Where(j => !j.IsDeleted) .Include(j => j.Customer) .Include(j => j.JobStatus) .Include(j => j.JobPriority) .Include(j => j.AssignedUser) .OrderBy(j => j.DueDate.HasValue ? 0 : 1) .ThenBy(j => j.DueDate) .ThenBy(j => j.JobPriority!.DisplayOrder) .ToListAsync(); } /// public async Task LoadForDetailsAsync(int id) { // Single query replaces the per-item N+1 loop that was in JobsController.Details. // EF Core splits the multi-level ThenIncludes across two SQL queries automatically // (split query behavior), keeping result set size manageable. return await _context.Jobs .Where(j => j.Id == id && !j.IsDeleted) .Include(j => j.Customer) .Include(j => j.JobStatus) .Include(j => j.JobPriority) .Include(j => j.AssignedUser) .Include(j => j.Quote) .Include(j => j.OvenCost) .Include(j => j.OriginalJob) .Include(j => j.IntakeCheckedBy) .Include(j => j.JobItems.Where(ji => !ji.IsDeleted)) .ThenInclude(ji => ji.Coats) .ThenInclude(c => c.InventoryItem) .Include(j => j.JobItems.Where(ji => !ji.IsDeleted)) .ThenInclude(ji => ji.Coats) .ThenInclude(c => c.Vendor) .Include(j => j.JobItems.Where(ji => !ji.IsDeleted)) .ThenInclude(ji => ji.PrepServices) .ThenInclude(ps => ps.PrepService) .Include(j => j.JobPrepServices.Where(jps => !jps.IsDeleted)) .ThenInclude(jps => jps.PrepService) .FirstOrDefaultAsync(); } /// public async Task LoadForEditAsync(int id) { return await _context.Jobs .Where(j => j.Id == id && !j.IsDeleted) .Include(j => j.Customer) .Include(j => j.JobStatus) .Include(j => j.JobPriority) .Include(j => j.AssignedUser) .Include(j => j.OvenCost) .Include(j => j.JobItems.Where(ji => !ji.IsDeleted)) .ThenInclude(ji => ji.Coats) .ThenInclude(c => c.InventoryItem) .Include(j => j.JobItems.Where(ji => !ji.IsDeleted)) .ThenInclude(ji => ji.Coats) .ThenInclude(c => c.Vendor) .Include(j => j.JobItems.Where(ji => !ji.IsDeleted)) .ThenInclude(ji => ji.PrepServices) .ThenInclude(ps => ps.PrepService) .Include(j => j.JobPrepServices.Where(jps => !jps.IsDeleted)) .ThenInclude(jps => jps.PrepService) .FirstOrDefaultAsync(); } /// public async Task LoadForStatusChangeAsync(int id) { return await _context.Jobs .Include(j => j.JobStatus) .FirstOrDefaultAsync(j => j.Id == id && !j.IsDeleted); } /// public async Task> GetChangeHistoryAsync(int jobId) { return await _context.JobChangeHistories .Where(h => h.JobId == jobId && !h.IsDeleted) .Include(h => h.ChangedBy) .OrderByDescending(h => h.ChangedAt) .AsNoTracking() .ToListAsync(); } /// public async Task 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(); } /// public async Task GetOrphanedConversionJobAsync(int quoteId, int companyId) { return await _context.Jobs .IgnoreQueryFilters() .FirstOrDefaultAsync(j => j.QuoteId == quoteId && j.CompanyId == companyId); } /// public async Task> 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(); } /// public async Task> 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(); } /// public async Task 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)) .AsNoTracking() .FirstOrDefaultAsync(); } /// public async Task LoadForTemplateSnapshotAsync(int jobId) { return await _context.Jobs .Where(j => j.Id == jobId && !j.IsDeleted) .Include(j => j.JobItems.Where(i => !i.IsDeleted)) .ThenInclude(i => i.Coats.Where(c => !c.IsDeleted)) .Include(j => j.JobItems.Where(i => !i.IsDeleted)) .ThenInclude(i => i.PrepServices.Where(p => !p.IsDeleted)) .FirstOrDefaultAsync(); } /// public async Task> GetOverdueScheduledJobsAsync() { var today = DateTime.Today; return await _context.Jobs .Include(j => j.Customer) .Include(j => j.JobStatus) .Include(j => j.JobPriority) .Include(j => j.AssignedUser) .Where(j => j.ScheduledDate.HasValue && j.ScheduledDate.Value.Date < today && !j.IsDeleted && !j.JobStatus.IsTerminalStatus) .OrderBy(j => j.ScheduledDate) .ThenBy(j => j.JobNumber) .ToListAsync(); } }