mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 10:43:13 +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.
115 lines
5.5 KiB
C#
115 lines
5.5 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using SeaHaven.DataServices.Helpers;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.DataServices.Models;
|
|
|
|
namespace SeaHaven.DataServices.Implementation
|
|
{
|
|
public class NotificationFeedDataService : INotificationFeedDataService
|
|
{
|
|
private static readonly string[] TerminalDispatchStatuses = { "Completed", "Verified", "Cancelled", "Canceled" };
|
|
|
|
private readonly ApplicationDbContext _context;
|
|
|
|
public NotificationFeedDataService(ApplicationDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
public async Task<UnassignedWorkOrdersSummary> GetUnassignedSummaryAsync(
|
|
int? accountId, CancellationToken cancellationToken)
|
|
{
|
|
var summary = await OpenWorkOrders(new NotificationFeedScope(accountId, null))
|
|
.Where(w => w.AssignTo == null || w.AssignTo == "")
|
|
.GroupBy(_ => 1)
|
|
.Select(group => new UnassignedWorkOrdersSummary
|
|
{
|
|
Count = group.Count(),
|
|
LatestCreatedAt = group.Max(w => w.CreatedDate)
|
|
})
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
|
|
return summary ?? new UnassignedWorkOrdersSummary();
|
|
}
|
|
|
|
public Task<NotificationCandidatePage> GetAvetaMissingAsync(
|
|
NotificationFeedScope scope, DateOnly today, int limit, CancellationToken cancellationToken)
|
|
{
|
|
// Same rule as the dashboard's Aveta Pending tile so the two counts agree.
|
|
var windowStart = today.ToDateTime(TimeOnly.MinValue);
|
|
var windowEnd = today.AddDays(2).ToDateTime(TimeOnly.MinValue);
|
|
var candidates = OpenWorkOrders(scope)
|
|
.Where(w => w.AvetaRequired
|
|
&& w.ScheduledDate >= windowStart
|
|
&& w.ScheduledDate < windowEnd
|
|
&& !w.workOrderAttachments!.Any(attachment =>
|
|
(attachment.IsDeleted == null || attachment.IsDeleted == false)
|
|
&& attachment.Category == WorkOrderMediaCategory.Aveta));
|
|
|
|
return PageAsync(candidates, limit, cancellationToken);
|
|
}
|
|
|
|
public Task<NotificationCandidatePage> GetNoVendorAsync(
|
|
NotificationFeedScope scope, DateTime cutoff, int limit, CancellationToken cancellationToken)
|
|
{
|
|
// "Has a vendor" matches the vendor reminders: any live non-terminal dispatch that
|
|
// points at the work order directly or links it through DispatchWorkOrders. Soft-deleted
|
|
// dispatches are ignored so a removed dispatch does not keep the row out of No Vendor.
|
|
var candidates = OpenWorkOrders(scope)
|
|
.Where(w => w.ScheduledDate.HasValue
|
|
&& w.ScheduledDate.Value <= cutoff
|
|
&& !_context.Dispatches.Any(d => d.WorkOrderId == w.Id
|
|
&& (d.IsDeleted == null || d.IsDeleted == false)
|
|
&& !TerminalDispatchStatuses.Contains(d.Status ?? ""))
|
|
&& !_context.DispatchWorkOrders.Any(link => link.WorkOrderId == w.Id
|
|
&& link.Dispatch != null
|
|
&& (link.Dispatch.IsDeleted == null || link.Dispatch.IsDeleted == false)
|
|
&& !TerminalDispatchStatuses.Contains(link.Dispatch.Status ?? "")));
|
|
|
|
return PageAsync(candidates, limit, cancellationToken);
|
|
}
|
|
|
|
private IQueryable<WorkOrder> OpenWorkOrders(NotificationFeedScope scope)
|
|
{
|
|
var workOrders = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
|
if (scope.AccountId is int accountId)
|
|
workOrders = WorkOrderBoardQueryFilters.ApplyAccountScope(workOrders, accountId);
|
|
if (scope.DispatcherId != null)
|
|
workOrders = workOrders.Where(w => w.AssignTo == scope.DispatcherId);
|
|
|
|
return workOrders.Where(w => w.LifecycleStatus != LifecycleStatus.Completed
|
|
&& w.LifecycleStatus != LifecycleStatus.Canceled
|
|
&& (w.LifecycleStatus != null
|
|
|| ((w.LegacyStatus ?? w.Status) != "Done"
|
|
&& (w.LegacyStatus ?? w.Status) != "Completed"
|
|
&& (w.LegacyStatus ?? w.Status) != "Complete"
|
|
&& (w.LegacyStatus ?? w.Status) != "Closed"
|
|
&& (w.LegacyStatus ?? w.Status) != "Canceled"
|
|
&& (w.LegacyStatus ?? w.Status) != "Cancelled")));
|
|
}
|
|
|
|
private static async Task<NotificationCandidatePage> PageAsync(
|
|
IQueryable<WorkOrder> candidates, int limit, CancellationToken cancellationToken)
|
|
{
|
|
var total = await candidates.CountAsync(cancellationToken);
|
|
if (total == 0)
|
|
return new NotificationCandidatePage();
|
|
|
|
var items = await candidates
|
|
.OrderByDescending(w => w.ScheduledDate)
|
|
.ThenByDescending(w => w.Id)
|
|
.Take(limit)
|
|
.Select(w => new NotificationWorkOrderCandidate
|
|
{
|
|
Id = w.Id,
|
|
Number = w.InternalWONumber ?? w.WorkerOrderNumber,
|
|
ScheduledDate = w.ScheduledDate!.Value
|
|
})
|
|
.ToListAsync(cancellationToken);
|
|
|
|
return new NotificationCandidatePage { Total = total, Items = items };
|
|
}
|
|
}
|
|
}
|