mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 17:43:12 +00:00
No Vendor treated any non-terminal dispatch as an assigned vendor without checking IsDeleted, so a soft-deleted dispatch hid the work order from both No Vendor and Vendor Conflict (the conflict query already drops deleted dispatches). GetNoVendorAsync and the vendor-reminder assigned-work-order rule now skip soft-deleted dispatches, keeping the asserted parity between the feed and the reminders. Vendor Conflict applied the section cap in vendor-sweep order, so when overlaps exceeded the limit newer conflicts could be dropped while Count still counted every work order. VendorConflictsAsync now orders pairs by recency before the cap, matching the other per-work-order sections.
249 lines
13 KiB
C#
249 lines
13 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.DataServices.Models;
|
|
|
|
namespace SeaHaven.DataServices.Implementation
|
|
{
|
|
public class VendorOperationsDataService : IVendorOperationsDataService
|
|
{
|
|
private static readonly string[] TerminalDispatchStatuses = { "Completed", "Verified", "Cancelled", "Canceled" };
|
|
|
|
private readonly ApplicationDbContext _context;
|
|
private readonly IDispatchDataService _dispatchDataService;
|
|
|
|
public VendorOperationsDataService(ApplicationDbContext context, IDispatchDataService dispatchDataService)
|
|
{
|
|
_context = context;
|
|
_dispatchDataService = dispatchDataService;
|
|
}
|
|
|
|
public async Task<IReadOnlyList<int>> GetAssignedWorkOrderIdsAsync(CancellationToken cancellationToken)
|
|
{
|
|
var directlyAssignedIds = await _context.Dispatches
|
|
.AsNoTracking()
|
|
.Where(d => !TerminalDispatchStatuses.Contains(d.Status ?? "")
|
|
&& (d.IsDeleted == null || d.IsDeleted == false)
|
|
&& d.WorkOrderId.HasValue)
|
|
.Select(d => d.WorkOrderId!.Value)
|
|
.ToListAsync(cancellationToken);
|
|
var multiplyAssignedIds = await _context.DispatchWorkOrders
|
|
.AsNoTracking()
|
|
.Where(link => link.Dispatch != null
|
|
&& (link.Dispatch.IsDeleted == null || link.Dispatch.IsDeleted == false)
|
|
&& !TerminalDispatchStatuses.Contains(link.Dispatch.Status ?? ""))
|
|
.Select(link => link.WorkOrderId)
|
|
.ToListAsync(cancellationToken);
|
|
return directlyAssignedIds.Concat(multiplyAssignedIds).Distinct().ToList();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<NoVendorCandidateReadModel>> GetNoVendorWorkOrdersAsync(
|
|
DateTime cutoff, IReadOnlyCollection<int> excludeIds, CancellationToken cancellationToken)
|
|
{
|
|
return await _context.workOrders
|
|
.AsNoTracking()
|
|
.Where(wo => wo.ScheduledDate.HasValue
|
|
&& wo.ScheduledDate.Value <= cutoff
|
|
&& wo.LifecycleStatus != LifecycleStatus.Completed
|
|
&& wo.LifecycleStatus != LifecycleStatus.Canceled
|
|
&& !excludeIds.Contains(wo.Id))
|
|
.OrderBy(wo => wo.ScheduledDate)
|
|
.Select(wo => new NoVendorCandidateReadModel
|
|
{
|
|
Id = wo.Id,
|
|
InternalWONumber = wo.InternalWONumber,
|
|
WorkerOrderNumber = wo.WorkerOrderNumber,
|
|
ScheduledDate = wo.ScheduledDate
|
|
})
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public Task<IReadOnlyList<ScheduledDispatchReadModel>> GetActiveScheduledDispatchesAsync(CancellationToken cancellationToken)
|
|
=> GetActiveScheduledDispatchesAsync(null, cancellationToken);
|
|
|
|
public async Task<IReadOnlyList<ScheduledDispatchReadModel>> GetActiveScheduledDispatchesAsync(
|
|
int? accountId, CancellationToken cancellationToken)
|
|
{
|
|
var directDispatches = _context.Dispatches.AsNoTracking()
|
|
.Where(dispatch => !TerminalDispatchStatuses.Contains(dispatch.Status ?? "")
|
|
&& (dispatch.IsDeleted == null || dispatch.IsDeleted == false)
|
|
&& dispatch.WorkOrderId.HasValue
|
|
&& (dispatch.ScheduledDate.HasValue
|
|
|| (dispatch.WorkOrder != null && dispatch.WorkOrder.ScheduledStart.HasValue)));
|
|
if (accountId is int directAccountId)
|
|
directDispatches = directDispatches.Where(dispatch =>
|
|
dispatch.WorkOrder != null && dispatch.WorkOrder.AccountId == directAccountId);
|
|
|
|
var direct = directDispatches
|
|
.Select(dispatch => new ScheduledDispatchReadModel
|
|
{
|
|
Id = dispatch.Id,
|
|
VendorId = dispatch.VendorId,
|
|
VendorName = dispatch.Vendor!.CompanyName,
|
|
WorkOrderId = dispatch.WorkOrderId!.Value,
|
|
WorkOrderNumber = dispatch.WorkOrder!.InternalWONumber ?? dispatch.WorkOrder.WorkerOrderNumber,
|
|
AssignTo = dispatch.WorkOrder.AssignTo,
|
|
Start = dispatch.WorkOrder!.ScheduledStart ?? dispatch.ScheduledDate!.Value,
|
|
End = dispatch.WorkOrder.ScheduledEnd
|
|
?? (dispatch.WorkOrder.ScheduledStart ?? dispatch.ScheduledDate!.Value).AddHours(2)
|
|
});
|
|
|
|
var linkedDispatches = _context.DispatchWorkOrders.AsNoTracking()
|
|
.Where(link => link.Dispatch != null
|
|
&& link.WorkOrder != null
|
|
&& link.Dispatch.WorkOrderId != link.WorkOrderId
|
|
&& !TerminalDispatchStatuses.Contains(link.Dispatch.Status ?? "")
|
|
&& (link.Dispatch.IsDeleted == null || link.Dispatch.IsDeleted == false)
|
|
&& (link.Dispatch.ScheduledDate.HasValue || link.WorkOrder.ScheduledStart.HasValue));
|
|
if (accountId is int linkedAccountId)
|
|
linkedDispatches = linkedDispatches.Where(link => link.WorkOrder!.AccountId == linkedAccountId);
|
|
|
|
var linked = linkedDispatches
|
|
.Select(link => new ScheduledDispatchReadModel
|
|
{
|
|
Id = link.DispatchId,
|
|
VendorId = link.Dispatch!.VendorId,
|
|
VendorName = link.Dispatch.Vendor!.CompanyName,
|
|
WorkOrderId = link.WorkOrderId,
|
|
WorkOrderNumber = link.WorkOrder!.InternalWONumber ?? link.WorkOrder.WorkerOrderNumber,
|
|
AssignTo = link.WorkOrder.AssignTo,
|
|
Start = link.WorkOrder.ScheduledStart ?? link.Dispatch.ScheduledDate!.Value,
|
|
End = link.WorkOrder.ScheduledEnd
|
|
?? (link.WorkOrder.ScheduledStart ?? link.Dispatch.ScheduledDate!.Value).AddHours(2)
|
|
});
|
|
|
|
return await direct.Concat(linked).ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<ActiveVendorReadModel>> GetActiveVendorsAsync(CancellationToken cancellationToken)
|
|
{
|
|
return await _context.Vendors.AsNoTracking()
|
|
.Where(v => v.IsActive && (v.IsDeleted == null || v.IsDeleted == false))
|
|
.Select(v => new ActiveVendorReadModel
|
|
{
|
|
Id = v.Id,
|
|
CompanyName = v.CompanyName,
|
|
ContactName = v.ContactName,
|
|
TradeSpecialties = v.TradeSpecialties,
|
|
AvailabilityStatus = v.AvailabilityStatus,
|
|
AvailabilityUpdatedAt = v.AvailabilityUpdatedAt
|
|
})
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<SitePreferredVendor>> GetSitePreferredVendorsAsync(int locationId, CancellationToken cancellationToken)
|
|
{
|
|
return await _context.SitePreferredVendors
|
|
.AsNoTracking()
|
|
.Where(p => p.LocationId == locationId)
|
|
.Include(p => p.Vendor)
|
|
.OrderBy(p => p.SortOrder)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<List<SitePreferredVendor>> GetSitePreferredVendorsForReplaceAsync(int locationId, CancellationToken cancellationToken)
|
|
{
|
|
return await _context.SitePreferredVendors
|
|
.Where(p => p.LocationId == locationId)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public Task<bool> LocationExistsAsync(int locationId, CancellationToken cancellationToken)
|
|
=> _context.Locations.AnyAsync(location => location.Id == locationId, cancellationToken);
|
|
|
|
public async Task<Dictionary<int, Vendor>> GetVendorsByIdsAsync(IReadOnlyCollection<int> vendorIds, CancellationToken cancellationToken)
|
|
=> await _context.Vendors.Where(vendor => vendorIds.Contains(vendor.Id))
|
|
.ToDictionaryAsync(vendor => vendor.Id, cancellationToken);
|
|
|
|
public async Task<IReadOnlyList<InsightDispatchData>> GetInsightDispatchDataAsync(
|
|
DateTime? from, DateTime? to, int? locationId, string? trade, int? vendorId, CancellationToken cancellationToken)
|
|
{
|
|
var query = _context.Dispatches.AsNoTracking()
|
|
.Include(dispatch => dispatch.Vendor)
|
|
.Include(dispatch => dispatch.WorkOrder)
|
|
.Where(dispatch => dispatch.Vendor != null);
|
|
if (from.HasValue) query = query.Where(dispatch => (dispatch.DispatchedAt ?? dispatch.CreatedDate) >= from);
|
|
if (to.HasValue) query = query.Where(dispatch => (dispatch.DispatchedAt ?? dispatch.CreatedDate) <= to);
|
|
if (locationId.HasValue) query = query.Where(dispatch => dispatch.WorkOrder!.LocationId == locationId);
|
|
if (!string.IsNullOrWhiteSpace(trade)) query = query.Where(dispatch => dispatch.WorkOrder!.Trade == trade);
|
|
if (vendorId.HasValue) query = query.Where(dispatch => dispatch.VendorId == vendorId);
|
|
|
|
return await query.Select(dispatch => new InsightDispatchData
|
|
{
|
|
VendorId = dispatch.VendorId,
|
|
VendorName = dispatch.Vendor!.CompanyName,
|
|
Status = dispatch.Status,
|
|
DispatchedAt = dispatch.DispatchedAt,
|
|
CreatedDate = dispatch.CreatedDate,
|
|
ScheduledDate = dispatch.ScheduledDate,
|
|
AcknowledgedAt = dispatch.AcknowledgedAt,
|
|
CompletedDate = dispatch.CompletedDate,
|
|
DueDate = dispatch.WorkOrder != null ? dispatch.WorkOrder.DueDate : null
|
|
}).ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public Task<Vendor?> GetVendorForUpdateAsync(int vendorId, CancellationToken cancellationToken)
|
|
=> _context.Vendors.FirstOrDefaultAsync(v => v.Id == vendorId, cancellationToken);
|
|
|
|
public Task<VendorCompletionDocument?> GetCompletionDocumentForUpdateAsync(int documentId, CancellationToken cancellationToken)
|
|
=> _context.VendorCompletionDocuments.FirstOrDefaultAsync(d => d.Id == documentId, cancellationToken);
|
|
|
|
public Task<Dispatch?> GetDispatchForUpdateAsync(int dispatchId, CancellationToken cancellationToken)
|
|
=> _context.Dispatches.FirstOrDefaultAsync(d => d.Id == dispatchId, cancellationToken);
|
|
|
|
public Task<WorkOrder?> GetWorkOrderForUpdateAsync(int workOrderId, CancellationToken cancellationToken)
|
|
=> _context.workOrders.FirstOrDefaultAsync(w => w.Id == workOrderId, cancellationToken);
|
|
|
|
public async Task<Dispatch?> GetExistingActiveDispatchForWorkOrderAsync(int workOrderId, CancellationToken cancellationToken)
|
|
{
|
|
var linkedDispatchIds = _context.DispatchWorkOrders
|
|
.Where(link => link.WorkOrderId == workOrderId)
|
|
.Select(link => link.DispatchId);
|
|
return await _context.Dispatches
|
|
.Where(item => (item.WorkOrderId == workOrderId || linkedDispatchIds.Contains(item.Id))
|
|
&& !TerminalDispatchStatuses.Contains(item.Status ?? ""))
|
|
.OrderByDescending(item => item.CreatedDate)
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
}
|
|
|
|
public Task<string> GenerateNextDispatchNumberAsync(CancellationToken cancellationToken)
|
|
=> _dispatchDataService.GenerateNextDispatchNumberAsync();
|
|
|
|
public Task<string> GenerateNextPONumberAsync(CancellationToken cancellationToken)
|
|
=> _dispatchDataService.GenerateNextPONumberAsync();
|
|
|
|
public Task AddDispatchAsync(Dispatch dispatch, CancellationToken cancellationToken)
|
|
{
|
|
_context.Dispatches.Add(dispatch);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task StageWorkOrderAuditAsync(WorkOrderAuditLog log, CancellationToken cancellationToken)
|
|
{
|
|
_context.WorkOrderAuditLogs.Add(log);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task StageVendorAuditAsync(VendorAuditLog log, CancellationToken cancellationToken)
|
|
{
|
|
_context.VendorAuditLogs.Add(log);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task AddSitePreferredVendorAsync(SitePreferredVendor preference, CancellationToken cancellationToken)
|
|
{
|
|
_context.SitePreferredVendors.Add(preference);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task RemoveSitePreferredVendorsAsync(IEnumerable<SitePreferredVendor> preferences, CancellationToken cancellationToken)
|
|
{
|
|
_context.SitePreferredVendors.RemoveRange(preferences);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<int> SaveChangesAsync(CancellationToken cancellationToken)
|
|
=> _context.SaveChangesAsync(cancellationToken);
|
|
}
|
|
}
|