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:
@@ -15,7 +15,6 @@ using PowderCoating.Application.Services;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Enums;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
using PowderCoating.Web.Helpers;
|
||||
using PowderCoating.Web.Hubs;
|
||||
|
||||
@@ -29,7 +28,6 @@ public class JobsController : Controller
|
||||
private readonly IJobPhotoService _jobPhotoService;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ILogger<JobsController> _logger;
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
private readonly IMeasurementConversionService _measurementService;
|
||||
private readonly ILookupCacheService _lookupCache;
|
||||
@@ -45,7 +43,6 @@ public class JobsController : Controller
|
||||
IJobPhotoService jobPhotoService,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ILogger<JobsController> logger,
|
||||
ApplicationDbContext context,
|
||||
ITenantContext tenantContext,
|
||||
IMeasurementConversionService measurementService,
|
||||
ILookupCacheService lookupCache,
|
||||
@@ -60,7 +57,6 @@ public class JobsController : Controller
|
||||
_jobPhotoService = jobPhotoService;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_context = context;
|
||||
_tenantContext = tenantContext;
|
||||
_measurementService = measurementService;
|
||||
_lookupCache = lookupCache;
|
||||
@@ -239,17 +235,7 @@ public class JobsController : Controller
|
||||
.ToList();
|
||||
|
||||
// Load all active jobs with related data
|
||||
var jobs = 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();
|
||||
var jobs = await _unitOfWork.Jobs.GetBoardJobsAsync();
|
||||
|
||||
var now = DateTime.UtcNow.Date;
|
||||
|
||||
@@ -296,15 +282,13 @@ public class JobsController : Controller
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> MoveCard([FromBody] MoveCardRequest req)
|
||||
{
|
||||
var job = await _context.Jobs
|
||||
.Include(j => j.JobStatus)
|
||||
.FirstOrDefaultAsync(j => j.Id == req.JobId && !j.IsDeleted);
|
||||
var job = await _unitOfWork.Jobs.LoadForStatusChangeAsync(req.JobId);
|
||||
|
||||
if (job == null)
|
||||
return Json(new { success = false, message = "Job not found." });
|
||||
|
||||
var newStatus = await _context.JobStatusLookups
|
||||
.FirstOrDefaultAsync(s => s.Id == req.NewStatusId && s.IsActive);
|
||||
var newStatus = await _unitOfWork.JobStatusLookups.FirstOrDefaultAsync(
|
||||
s => s.Id == req.NewStatusId && s.IsActive);
|
||||
|
||||
if (newStatus == null)
|
||||
return Json(new { success = false, message = "Status not found." });
|
||||
@@ -314,7 +298,7 @@ public class JobsController : Controller
|
||||
job.UpdatedAt = DateTime.UtcNow;
|
||||
job.UpdatedBy = User.Identity?.Name;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
await BroadcastJobUpdate(job.CompanyId, job.JobNumber!, job.Id, "StatusChanged",
|
||||
$"Status → {newStatus.DisplayName}");
|
||||
@@ -354,49 +338,12 @@ public class JobsController : Controller
|
||||
|
||||
try
|
||||
{
|
||||
var job = await _unitOfWork.Jobs.GetByIdAsync(id.Value, false,
|
||||
j => j.Customer,
|
||||
j => j.JobStatus,
|
||||
j => j.JobPriority,
|
||||
j => j.JobItems,
|
||||
j => j.AssignedUser,
|
||||
j => j.Quote,
|
||||
j => j.OvenCost,
|
||||
j => j.OriginalJob,
|
||||
j => j.IntakeCheckedBy);
|
||||
var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id.Value);
|
||||
if (job == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// Load JobItemCoats and PrepServices for each JobItem
|
||||
foreach (var item in job.JobItems)
|
||||
{
|
||||
var itemWithDetails = await _context.JobItems
|
||||
.Where(ji => ji.Id == item.Id)
|
||||
.Include(ji => ji.Coats)
|
||||
.ThenInclude(c => c.InventoryItem)
|
||||
.Include(ji => ji.Coats)
|
||||
.ThenInclude(c => c.Vendor)
|
||||
.Include(ji => ji.PrepServices)
|
||||
.ThenInclude(ps => ps.PrepService)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (itemWithDetails?.Coats != null)
|
||||
item.Coats = itemWithDetails.Coats;
|
||||
if (itemWithDetails?.PrepServices != null)
|
||||
item.PrepServices = itemWithDetails.PrepServices;
|
||||
}
|
||||
|
||||
// Load prep services for this job
|
||||
var jobPrepServices = await _context.JobPrepServices
|
||||
.Where(jps => jps.JobId == id.Value && !jps.IsDeleted)
|
||||
.Include(jps => jps.PrepService)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
job.JobPrepServices = jobPrepServices;
|
||||
|
||||
// Build photo tag suggestions from coat colors on this job
|
||||
var coatColorSuggestions = job.JobItems
|
||||
.SelectMany(ji => ji.Coats)
|
||||
@@ -411,13 +358,7 @@ public class JobsController : Controller
|
||||
var jobDto = _mapper.Map<JobDto>(job);
|
||||
|
||||
// Load change history
|
||||
var changeHistories = await _context.JobChangeHistories
|
||||
.Where(h => h.JobId == id.Value && !h.IsDeleted)
|
||||
.Include(h => h.ChangedBy)
|
||||
.OrderByDescending(h => h.ChangedAt)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
var changeHistories = await _unitOfWork.Jobs.GetChangeHistoryAsync(id.Value);
|
||||
_logger.LogInformation("Loaded {Count} change history records for Job {JobId}", changeHistories.Count, id.Value);
|
||||
|
||||
var changeHistoryDtos = _mapper.Map<List<JobChangeHistoryDto>>(changeHistories);
|
||||
@@ -435,10 +376,7 @@ public class JobsController : Controller
|
||||
ViewBag.AreaUnit = _measurementService.GetAreaUnitLabel(useMetric);
|
||||
|
||||
// Check if an invoice exists for this job
|
||||
var jobInvoice = await _context.Set<Invoice>()
|
||||
.Where(i => i.JobId == id.Value && !i.IsDeleted)
|
||||
.Select(i => new { i.Id, i.InvoiceNumber, i.Status })
|
||||
.FirstOrDefaultAsync();
|
||||
var jobInvoice = await _unitOfWork.Invoices.GetForJobAsync(id.Value);
|
||||
ViewBag.JobInvoiceId = jobInvoice?.Id;
|
||||
ViewBag.JobInvoiceNumber = jobInvoice?.InvoiceNumber;
|
||||
ViewBag.JobInvoiceStatus = jobInvoice?.Status;
|
||||
@@ -498,21 +436,16 @@ public class JobsController : Controller
|
||||
}).ToList();
|
||||
|
||||
// Load deposits for this job (and any linked quote)
|
||||
var depositQuery = _context.Set<Deposit>()
|
||||
.Where(d => !d.IsDeleted && d.CompanyId == job.CompanyId
|
||||
&& (d.JobId == id.Value || (job.QuoteId.HasValue && d.QuoteId == job.QuoteId.Value)))
|
||||
.Include(d => d.RecordedBy)
|
||||
.OrderByDescending(d => d.ReceivedDate)
|
||||
.AsNoTracking();
|
||||
ViewBag.Deposits = await depositQuery.ToListAsync();
|
||||
var jobDeposits = (await _unitOfWork.Deposits.FindAsync(
|
||||
d => d.JobId == id.Value || (job.QuoteId.HasValue && d.QuoteId == job.QuoteId.Value),
|
||||
false, d => d.RecordedBy))
|
||||
.OrderByDescending(d => d.ReceivedDate).ToList();
|
||||
ViewBag.Deposits = jobDeposits;
|
||||
|
||||
// Materials used on this job via QR scan or manual log
|
||||
ViewBag.MaterialsUsed = await _context.Set<InventoryTransaction>()
|
||||
.Where(t => !t.IsDeleted && t.JobId == id.Value)
|
||||
.Include(t => t.InventoryItem)
|
||||
.OrderByDescending(t => t.TransactionDate)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
ViewBag.MaterialsUsed = (await _unitOfWork.InventoryTransactions.FindAsync(
|
||||
t => t.JobId == id.Value, false, t => t.InventoryItem))
|
||||
.OrderByDescending(t => t.TransactionDate).ToList();
|
||||
|
||||
// Job photo subscription limits — used to disable the upload button in the view
|
||||
var photoCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
@@ -650,46 +583,12 @@ public class JobsController : Controller
|
||||
|
||||
try
|
||||
{
|
||||
var job = await _unitOfWork.Jobs.GetByIdAsync(id.Value, false,
|
||||
j => j.Customer,
|
||||
j => j.JobStatus,
|
||||
j => j.JobPriority,
|
||||
j => j.JobItems,
|
||||
j => j.AssignedUser,
|
||||
j => j.Quote);
|
||||
var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id.Value);
|
||||
if (job == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// Load JobItemCoats and PrepServices for each JobItem
|
||||
foreach (var item in job.JobItems)
|
||||
{
|
||||
var itemWithDetails = await _context.JobItems
|
||||
.Where(ji => ji.Id == item.Id)
|
||||
.Include(ji => ji.Coats)
|
||||
.ThenInclude(c => c.InventoryItem)
|
||||
.Include(ji => ji.Coats)
|
||||
.ThenInclude(c => c.Vendor)
|
||||
.Include(ji => ji.PrepServices)
|
||||
.ThenInclude(ps => ps.PrepService)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (itemWithDetails?.Coats != null)
|
||||
item.Coats = itemWithDetails.Coats;
|
||||
if (itemWithDetails?.PrepServices != null)
|
||||
item.PrepServices = itemWithDetails.PrepServices;
|
||||
}
|
||||
|
||||
// Load prep services for this job
|
||||
var jobPrepServices = await _context.JobPrepServices
|
||||
.Where(jps => jps.JobId == id.Value && !jps.IsDeleted)
|
||||
.Include(jps => jps.PrepService)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
job.JobPrepServices = jobPrepServices;
|
||||
|
||||
// Use AutoMapper to map the job entity to JobDto
|
||||
var jobDto = _mapper.Map<JobDto>(job);
|
||||
|
||||
@@ -943,16 +842,23 @@ public class JobsController : Controller
|
||||
// Pre-populate from template if provided
|
||||
if (templateId.HasValue)
|
||||
{
|
||||
var template = await _context.JobTemplates
|
||||
.Include(t => t.Items.Where(i => !i.IsDeleted))
|
||||
.ThenInclude(i => i.Coats.Where(c => !c.IsDeleted))
|
||||
.Include(t => t.Items.Where(i => !i.IsDeleted))
|
||||
.ThenInclude(i => i.PrepServices.Where(p => !p.IsDeleted))
|
||||
.ThenInclude(p => p.PrepService)
|
||||
.FirstOrDefaultAsync(t => t.Id == templateId.Value && !t.IsDeleted);
|
||||
|
||||
var template = await _unitOfWork.JobTemplates.GetByIdAsync(templateId.Value);
|
||||
if (template != null)
|
||||
{
|
||||
var templateItemsEnum = await _unitOfWork.JobTemplateItems.FindAsync(
|
||||
i => i.JobTemplateId == templateId.Value, false, i => i.Coats, i => i.PrepServices);
|
||||
var templateItems = templateItemsEnum.ToList();
|
||||
|
||||
var tplPrepIds = templateItems
|
||||
.SelectMany(i => i.PrepServices.Select(p => p.PrepServiceId))
|
||||
.Distinct().ToList();
|
||||
Dictionary<int, string> tplPrepNameMap = new();
|
||||
if (tplPrepIds.Any())
|
||||
{
|
||||
var tplPreps = await _unitOfWork.PrepServices.FindAsync(p => tplPrepIds.Contains(p.Id));
|
||||
tplPrepNameMap = tplPreps.ToDictionary(p => p.Id, p => p.ServiceName);
|
||||
}
|
||||
|
||||
if (!customerId.HasValue && template.CustomerId.HasValue)
|
||||
dto.CustomerId = template.CustomerId.Value;
|
||||
dto.SpecialInstructions = template.SpecialInstructions;
|
||||
@@ -962,7 +868,7 @@ public class JobsController : Controller
|
||||
{
|
||||
id = template.Id,
|
||||
name = template.Name,
|
||||
items = template.Items.OrderBy(i => i.DisplayOrder).Select(i => new
|
||||
items = templateItems.OrderBy(i => i.DisplayOrder).Select(i => new
|
||||
{
|
||||
description = i.Description,
|
||||
quantity = i.Quantity,
|
||||
@@ -993,7 +899,7 @@ public class JobsController : Controller
|
||||
prepServices = i.PrepServices.Select(p => new
|
||||
{
|
||||
prepServiceId = p.PrepServiceId,
|
||||
prepServiceName = p.PrepService?.ServiceName,
|
||||
prepServiceName = tplPrepNameMap.TryGetValue(p.PrepServiceId, out var psName) ? psName : null,
|
||||
estimatedMinutes = p.EstimatedMinutes
|
||||
})
|
||||
})
|
||||
@@ -1071,14 +977,14 @@ public class JobsController : Controller
|
||||
{
|
||||
foreach (var prepServiceId in dto.PrepServiceIds)
|
||||
{
|
||||
await _context.JobPrepServices.AddAsync(new JobPrepService
|
||||
await _unitOfWork.JobPrepServices.AddAsync(new JobPrepService
|
||||
{
|
||||
JobId = job.Id,
|
||||
PrepServiceId = prepServiceId,
|
||||
CompanyId = companyId
|
||||
});
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
await _unitOfWork.CompleteAsync();
|
||||
}
|
||||
|
||||
// Save job items from wizard
|
||||
@@ -1154,7 +1060,7 @@ public class JobsController : Controller
|
||||
{
|
||||
foreach (var psDto in itemDto.PrepServices)
|
||||
{
|
||||
await _context.JobItemPrepServices.AddAsync(new JobItemPrepService
|
||||
await _unitOfWork.JobItemPrepServices.AddAsync(new JobItemPrepService
|
||||
{
|
||||
JobItemId = jobItem.Id,
|
||||
PrepServiceId = psDto.PrepServiceId,
|
||||
@@ -1212,26 +1118,12 @@ public class JobsController : Controller
|
||||
|
||||
try
|
||||
{
|
||||
var job = await _unitOfWork.Jobs.GetByIdAsync(id.Value, false, j => j.JobStatus, j => j.JobPriority);
|
||||
var job = await _unitOfWork.Jobs.LoadForEditAsync(id.Value);
|
||||
if (job == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// Load prep services for this job
|
||||
var jobPrepServices = await _context.JobPrepServices
|
||||
.Where(jps => jps.JobId == id.Value && !jps.IsDeleted)
|
||||
.Select(jps => jps.PrepServiceId)
|
||||
.ToListAsync();
|
||||
|
||||
// Load job items with full detail for wizard pre-fill
|
||||
var jobItemsWithDetail = await _context.JobItems
|
||||
.Where(ji => ji.JobId == id.Value && !ji.IsDeleted)
|
||||
.Include(ji => ji.Coats)
|
||||
.Include(ji => ji.PrepServices)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
var dto = new UpdateJobDto
|
||||
{
|
||||
Id = job.Id,
|
||||
@@ -1252,7 +1144,7 @@ public class JobsController : Controller
|
||||
DiscountType = job.DiscountType.ToString(),
|
||||
DiscountValue = job.DiscountValue,
|
||||
DiscountReason = job.DiscountReason,
|
||||
JobItems = jobItemsWithDetail.Select(ji => new CreateQuoteItemDto
|
||||
JobItems = job.JobItems.Where(ji => !ji.IsDeleted).Select(ji => new CreateQuoteItemDto
|
||||
{
|
||||
Description = ji.Description,
|
||||
Quantity = ji.Quantity,
|
||||
@@ -1289,7 +1181,7 @@ public class JobsController : Controller
|
||||
EstimatedMinutes = ps.EstimatedMinutes
|
||||
}).ToList()
|
||||
}).ToList(),
|
||||
PrepServiceIds = jobPrepServices
|
||||
PrepServiceIds = job.JobPrepServices.Select(jps => jps.PrepServiceId).ToList()
|
||||
};
|
||||
|
||||
var editCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
@@ -1339,9 +1231,8 @@ public class JobsController : Controller
|
||||
try
|
||||
{
|
||||
// Replace job items: soft-delete old, recreate from wizard
|
||||
var oldItems = await _context.JobItems
|
||||
.Where(ji => ji.JobId == id && !ji.IsDeleted)
|
||||
.ToListAsync();
|
||||
var oldItemsEnum = await _unitOfWork.JobItems.FindAsync(ji => ji.JobId == id);
|
||||
var oldItems = oldItemsEnum.ToList();
|
||||
var itemsChanged = dto.JobItems.Any() || oldItems.Any();
|
||||
|
||||
foreach (var oldItem in oldItems)
|
||||
@@ -1349,7 +1240,7 @@ public class JobsController : Controller
|
||||
oldItem.IsDeleted = true;
|
||||
oldItem.DeletedAt = DateTime.UtcNow;
|
||||
}
|
||||
if (oldItems.Any()) await _context.SaveChangesAsync();
|
||||
if (oldItems.Any()) await _unitOfWork.CompleteAsync();
|
||||
|
||||
foreach (var itemDto in dto.JobItems)
|
||||
{
|
||||
@@ -1419,7 +1310,7 @@ public class JobsController : Controller
|
||||
{
|
||||
foreach (var psDto in itemDto.PrepServices)
|
||||
{
|
||||
await _context.JobItemPrepServices.AddAsync(new JobItemPrepService
|
||||
await _unitOfWork.JobItemPrepServices.AddAsync(new JobItemPrepService
|
||||
{
|
||||
JobItemId = jobItem.Id,
|
||||
PrepServiceId = psDto.PrepServiceId,
|
||||
@@ -1698,11 +1589,13 @@ public class JobsController : Controller
|
||||
}
|
||||
|
||||
// Update prep services
|
||||
// Delete existing prep services
|
||||
var existingPrepServices = await _context.JobPrepServices
|
||||
.Where(jps => jps.JobId == job.Id && !jps.IsDeleted)
|
||||
.ToListAsync();
|
||||
_context.JobPrepServices.RemoveRange(existingPrepServices);
|
||||
// Soft-delete existing prep services
|
||||
var existingPrepServicesEnum = await _unitOfWork.JobPrepServices.FindAsync(jps => jps.JobId == job.Id);
|
||||
foreach (var eps in existingPrepServicesEnum)
|
||||
{
|
||||
eps.IsDeleted = true;
|
||||
eps.DeletedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// Add new prep services
|
||||
if (dto.PrepServiceIds != null && dto.PrepServiceIds.Any())
|
||||
@@ -1715,7 +1608,7 @@ public class JobsController : Controller
|
||||
PrepServiceId = prepServiceId,
|
||||
CompanyId = currentUser.CompanyId
|
||||
};
|
||||
await _context.JobPrepServices.AddAsync(jobPrepService);
|
||||
await _unitOfWork.JobPrepServices.AddAsync(jobPrepService);
|
||||
}
|
||||
_logger.LogInformation("Updated prep services for job {JobNumber}: {Count} services", job.JobNumber, dto.PrepServiceIds.Count);
|
||||
}
|
||||
@@ -1753,11 +1646,7 @@ public class JobsController : Controller
|
||||
_logger.LogWarning(ex, "Notification failed for job {Id}", job.Id);
|
||||
}
|
||||
|
||||
var editNotifLog = await _context.NotificationLogs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.JobId == job.Id)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.FirstOrDefaultAsync();
|
||||
var editNotifLog = await _unitOfWork.NotificationLogs.GetLatestForJobAsync(job.Id);
|
||||
this.SetNotificationResultToast(editNotifLog);
|
||||
}
|
||||
|
||||
@@ -1945,23 +1834,13 @@ public class JobsController : Controller
|
||||
var month = DateTime.Now.Month.ToString("D2");
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
|
||||
var prefs = await _context.CompanyPreferences
|
||||
.IgnoreQueryFilters()
|
||||
.Where(p => p.CompanyId == companyId && !p.IsDeleted)
|
||||
.Select(p => new { p.JobNumberPrefix })
|
||||
.FirstOrDefaultAsync();
|
||||
var prefs = await _unitOfWork.CompanyPreferences.FirstOrDefaultAsync(
|
||||
p => p.CompanyId == companyId && !p.IsDeleted, ignoreQueryFilters: true);
|
||||
|
||||
var jobPrefix = !string.IsNullOrWhiteSpace(prefs?.JobNumberPrefix) ? prefs.JobNumberPrefix : "JOB";
|
||||
var prefix = $"{jobPrefix}-{year}{month}";
|
||||
|
||||
// IgnoreQueryFilters so soft-deleted jobs are counted (prevents number reuse)
|
||||
// Explicit CompanyId filter scopes to current company only
|
||||
var lastJobNumber = await _context.Jobs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(j => j.CompanyId == companyId && j.JobNumber.StartsWith(prefix))
|
||||
.OrderByDescending(j => j.JobNumber)
|
||||
.Select(j => j.JobNumber)
|
||||
.FirstOrDefaultAsync();
|
||||
var lastJobNumber = await _unitOfWork.Jobs.GetLastJobNumberByPrefixAsync(companyId, prefix);
|
||||
|
||||
if (lastJobNumber != null)
|
||||
{
|
||||
@@ -1987,27 +1866,14 @@ public class JobsController : Controller
|
||||
var today = date?.Date ?? DateTime.Today;
|
||||
|
||||
// Load all non-terminal statuses for the progress strip (excluding nav/hold/cancel)
|
||||
var allStatuses = await _context.JobStatusLookups
|
||||
.Where(s => !s.IsDeleted && s.StatusCode != "ON_HOLD" && s.StatusCode != "CANCELLED"
|
||||
&& s.StatusCode != "DELIVERED" && s.StatusCode != "QUOTED"
|
||||
&& s.StatusCode != "PENDING" && s.StatusCode != "APPROVED")
|
||||
.OrderBy(s => s.DisplayOrder)
|
||||
.ToListAsync();
|
||||
var allStatusesEnum = await _unitOfWork.JobStatusLookups.FindAsync(s =>
|
||||
s.StatusCode != "ON_HOLD" && s.StatusCode != "CANCELLED"
|
||||
&& s.StatusCode != "DELIVERED" && s.StatusCode != "QUOTED"
|
||||
&& s.StatusCode != "PENDING" && s.StatusCode != "APPROVED");
|
||||
var allStatuses = allStatusesEnum.OrderBy(s => s.DisplayOrder).ToList();
|
||||
|
||||
// Get all jobs scheduled for today with related data including items and coats
|
||||
var jobQuery = _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 == today && !j.IsDeleted && !j.JobStatus.IsTerminalStatus);
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
jobQuery = jobQuery.Where(j => j.AssignedUserId == userId);
|
||||
|
||||
var jobs = await jobQuery.ToListAsync();
|
||||
var jobs = await _unitOfWork.Jobs.GetScheduledJobsForDateAsync(today, userId);
|
||||
|
||||
// Get existing priority records for today
|
||||
var existingPriorities = await _unitOfWork.JobDailyPriorities
|
||||
@@ -2107,27 +1973,13 @@ public class JobsController : Controller
|
||||
var companyId = _tenantContext.GetCurrentCompanyId();
|
||||
if (!companyId.HasValue) return RedirectToAction(nameof(Index));
|
||||
|
||||
var allStatuses = await _context.JobStatusLookups
|
||||
.Where(s => !s.IsDeleted && !s.IsTerminalStatus
|
||||
&& s.StatusCode != "ON_HOLD" && s.StatusCode != "CANCELLED"
|
||||
&& s.StatusCode != "DELIVERED")
|
||||
.OrderBy(s => s.DisplayOrder)
|
||||
.ToListAsync();
|
||||
var allStatusesEnum = await _unitOfWork.JobStatusLookups.FindAsync(s =>
|
||||
!s.IsTerminalStatus
|
||||
&& s.StatusCode != "ON_HOLD" && s.StatusCode != "CANCELLED"
|
||||
&& s.StatusCode != "DELIVERED");
|
||||
var allStatuses = allStatusesEnum.OrderBy(s => s.DisplayOrder).ToList();
|
||||
|
||||
var jobQuery = _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))
|
||||
jobQuery = jobQuery.Where(j => j.AssignedUserId == workerId);
|
||||
|
||||
var jobs = await jobQuery.ToListAsync();
|
||||
var jobs = await _unitOfWork.Jobs.GetActiveJobsForMobileAsync(companyId.Value, workerId);
|
||||
|
||||
var jobDtos = jobs.Select(j =>
|
||||
{
|
||||
@@ -2251,12 +2103,10 @@ public class JobsController : Controller
|
||||
{
|
||||
try
|
||||
{
|
||||
var job = await _context.Jobs
|
||||
.Include(j => j.JobStatus)
|
||||
.FirstOrDefaultAsync(j => j.Id == request.JobId && !j.IsDeleted);
|
||||
var job = await _unitOfWork.Jobs.LoadForStatusChangeAsync(request.JobId);
|
||||
if (job == null) return Json(new { success = false, message = "Job not found" });
|
||||
|
||||
var newStatus = await _context.JobStatusLookups.FindAsync(request.NewStatusId);
|
||||
var newStatus = await _unitOfWork.JobStatusLookups.GetByIdAsync(request.NewStatusId);
|
||||
if (newStatus == null) return Json(new { success = false, message = "Status not found" });
|
||||
|
||||
var oldStatusId = job.JobStatusId;
|
||||
@@ -2330,9 +2180,8 @@ public class JobsController : Controller
|
||||
|
||||
/// <summary>
|
||||
/// AJAX endpoint for inline worker reassignment on the Job Details page.
|
||||
/// Uses EF ExecuteUpdateAsync (bulk update without loading the entity) for efficiency —
|
||||
/// no need to load the full job just to update one FK. The response includes the worker's
|
||||
/// display name so the UI can update the badge without a page reload.
|
||||
/// Loads the job, sets <see cref="Job.AssignedUserId"/>, and saves.
|
||||
/// The response includes the worker's display name so the UI can update the badge without a page reload.
|
||||
/// Passing WorkerId = null unassigns the current worker.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
@@ -2341,16 +2190,15 @@ public class JobsController : Controller
|
||||
{
|
||||
try
|
||||
{
|
||||
var rowsAffected = await _context.Jobs
|
||||
.Where(j => j.Id == request.JobId)
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(j => j.AssignedUserId, request.WorkerId)
|
||||
.SetProperty(j => j.UpdatedAt, DateTime.UtcNow));
|
||||
|
||||
if (rowsAffected == 0)
|
||||
var workerJob = await _unitOfWork.Jobs.GetByIdAsync(request.JobId);
|
||||
if (workerJob == null)
|
||||
{
|
||||
return Json(new { success = false, message = "Job not found" });
|
||||
}
|
||||
workerJob.AssignedUserId = request.WorkerId;
|
||||
workerJob.UpdatedAt = DateTime.UtcNow;
|
||||
await _unitOfWork.Jobs.UpdateAsync(workerJob);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
string? workerName = null;
|
||||
if (!string.IsNullOrEmpty(request.WorkerId))
|
||||
@@ -2359,13 +2207,9 @@ public class JobsController : Controller
|
||||
workerName = user?.FullName;
|
||||
}
|
||||
|
||||
var assignedJob = await _unitOfWork.Jobs.GetByIdAsync(request.JobId);
|
||||
if (assignedJob != null)
|
||||
{
|
||||
var assignDetail = workerName != null ? $"Assigned to {workerName}" : "Worker unassigned";
|
||||
await BroadcastJobUpdate(assignedJob.CompanyId, assignedJob.JobNumber!, assignedJob.Id,
|
||||
"WorkerChanged", assignDetail);
|
||||
}
|
||||
var assignDetail = workerName != null ? $"Assigned to {workerName}" : "Worker unassigned";
|
||||
await BroadcastJobUpdate(workerJob.CompanyId, workerJob.JobNumber!, workerJob.Id,
|
||||
"WorkerChanged", assignDetail);
|
||||
|
||||
return Json(new { success = true, workerName = workerName });
|
||||
}
|
||||
@@ -2476,11 +2320,7 @@ public class JobsController : Controller
|
||||
_logger.LogWarning(ex, "Notification failed for job {Id}", request.JobId);
|
||||
}
|
||||
|
||||
var statusNotifLog = await _context.NotificationLogs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.JobId == request.JobId)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.FirstOrDefaultAsync();
|
||||
var statusNotifLog = await _unitOfWork.NotificationLogs.GetLatestForJobAsync(request.JobId);
|
||||
this.SetNotificationResultToast(statusNotifLog);
|
||||
}
|
||||
|
||||
@@ -2545,14 +2385,8 @@ public class JobsController : Controller
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use AsNoTracking to fetch fresh data from database without EF caching
|
||||
var photos = await _context.JobPhotos
|
||||
.AsNoTracking()
|
||||
.Include(p => p.UploadedBy)
|
||||
.Where(p => p.JobId == jobId && !p.IsDeleted)
|
||||
.OrderBy(p => p.DisplayOrder)
|
||||
.ThenBy(p => p.UploadedDate)
|
||||
.ToListAsync();
|
||||
var photosEnum = await _unitOfWork.JobPhotos.FindAsync(p => p.JobId == jobId, false, p => p.UploadedBy);
|
||||
var photos = photosEnum.OrderBy(p => p.DisplayOrder).ThenBy(p => p.UploadedDate).ToList();
|
||||
|
||||
var photoDtos = photos
|
||||
.Select(p => _mapper.Map<JobPhotoDto>(p))
|
||||
@@ -2740,7 +2574,7 @@ public class JobsController : Controller
|
||||
job.CompletedDate = DateTime.UtcNow;
|
||||
|
||||
// Find the "Completed" status
|
||||
var completedStatus = await _context.JobStatusLookups
|
||||
var completedStatus = await _unitOfWork.JobStatusLookups
|
||||
.FirstOrDefaultAsync(s => s.StatusCode == "COMPLETED" && s.CompanyId == job.CompanyId);
|
||||
|
||||
if (completedStatus != null)
|
||||
@@ -2844,11 +2678,7 @@ public class JobsController : Controller
|
||||
_logger.LogWarning(ex, "Notification failed for job {Id}", dto.JobId);
|
||||
}
|
||||
|
||||
var completeNotifLog = await _context.NotificationLogs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.JobId == dto.JobId)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.FirstOrDefaultAsync();
|
||||
var completeNotifLog = await _unitOfWork.NotificationLogs.GetLatestForJobAsync(dto.JobId);
|
||||
this.SetNotificationResultToast(completeNotifLog);
|
||||
}
|
||||
|
||||
@@ -2999,9 +2829,8 @@ public class JobsController : Controller
|
||||
try
|
||||
{
|
||||
// Soft-delete existing job items (and their children)
|
||||
var oldItems = await _context.JobItems
|
||||
.Where(ji => ji.JobId == job.Id && !ji.IsDeleted)
|
||||
.ToListAsync();
|
||||
var oldItemsEnum = await _unitOfWork.JobItems.FindAsync(ji => ji.JobId == job.Id);
|
||||
var oldItems = oldItemsEnum.ToList();
|
||||
|
||||
foreach (var oldItem in oldItems)
|
||||
{
|
||||
@@ -3009,7 +2838,7 @@ public class JobsController : Controller
|
||||
oldItem.DeletedAt = DateTime.UtcNow;
|
||||
oldItem.DeletedBy = currentUser.UserName;
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// Create new items
|
||||
foreach (var itemDto in model.JobItems)
|
||||
@@ -3094,7 +2923,7 @@ public class JobsController : Controller
|
||||
CompanyId = currentUser.CompanyId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
await _context.JobItemPrepServices.AddAsync(ps);
|
||||
await _unitOfWork.JobItemPrepServices.AddAsync(ps);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3136,8 +2965,7 @@ public class JobsController : Controller
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
if (currentUser == null) return Unauthorized();
|
||||
|
||||
var item = await _context.JobItems
|
||||
.FirstOrDefaultAsync(ji => ji.Id == id && !ji.IsDeleted);
|
||||
var item = await _unitOfWork.JobItems.GetByIdAsync(id);
|
||||
if (item == null) return NotFound();
|
||||
|
||||
var job = await _unitOfWork.Jobs.GetByIdAsync(item.JobId);
|
||||
@@ -3146,14 +2974,11 @@ public class JobsController : Controller
|
||||
item.IsDeleted = true;
|
||||
item.DeletedAt = DateTime.UtcNow;
|
||||
item.DeletedBy = currentUser.UserName;
|
||||
await _context.SaveChangesAsync();
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// Recalculate job total from remaining items
|
||||
var remainingItems = await _context.JobItems
|
||||
.Where(ji => ji.JobId == job.Id && !ji.IsDeleted)
|
||||
.Include(ji => ji.Coats)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
var remainingItemsEnum = await _unitOfWork.JobItems.FindAsync(ji => ji.JobId == job.Id, false, ji => ji.Coats);
|
||||
var remainingItems = remainingItemsEnum.ToList();
|
||||
|
||||
var remainingDtos = remainingItems.Select(ji => new CreateQuoteItemDto
|
||||
{
|
||||
@@ -3475,12 +3300,7 @@ public class JobsController : Controller
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> AddReworkRecord([FromBody] CreateReworkRecordDto dto)
|
||||
{
|
||||
var job = await _context.Jobs
|
||||
.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(j => j.Id == dto.JobId && !j.IsDeleted);
|
||||
var job = await _unitOfWork.Jobs.LoadForDetailsAsync(dto.JobId);
|
||||
if (job == null) return NotFound();
|
||||
|
||||
var companyId = job.CompanyId;
|
||||
@@ -3572,7 +3392,7 @@ public class JobsController : Controller
|
||||
|
||||
foreach (var prep in item.PrepServices)
|
||||
{
|
||||
_context.Set<JobItemPrepService>().Add(new JobItemPrepService
|
||||
await _unitOfWork.JobItemPrepServices.AddAsync(new JobItemPrepService
|
||||
{
|
||||
JobItemId = newItem.Id,
|
||||
PrepServiceId = prep.PrepServiceId,
|
||||
@@ -3742,16 +3562,7 @@ public class JobsController : Controller
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
|
||||
// Load job with items, coats, and time entries
|
||||
var job = 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();
|
||||
var job = await _unitOfWork.Jobs.LoadForCostingAsync(jobId, companyId);
|
||||
|
||||
if (job == null) return NotFound();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user