2026-09-18 12:40:33 -03:00
|
|
|
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)
|
|
|
|
|
{
|
2026-09-18 12:59:56 -03:00
|
|
|
// "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.
|
2026-09-18 12:40:33 -03:00
|
|
|
var candidates = OpenWorkOrders(scope)
|
|
|
|
|
.Where(w => w.ScheduledDate.HasValue
|
|
|
|
|
&& w.ScheduledDate.Value <= cutoff
|
|
|
|
|
&& !_context.Dispatches.Any(d => d.WorkOrderId == w.Id
|
2026-09-18 12:59:56 -03:00
|
|
|
&& (d.IsDeleted == null || d.IsDeleted == false)
|
2026-09-18 12:40:33 -03:00
|
|
|
&& !TerminalDispatchStatuses.Contains(d.Status ?? ""))
|
|
|
|
|
&& !_context.DispatchWorkOrders.Any(link => link.WorkOrderId == w.Id
|
|
|
|
|
&& link.Dispatch != null
|
2026-09-18 12:59:56 -03:00
|
|
|
&& (link.Dispatch.IsDeleted == null || link.Dispatch.IsDeleted == false)
|
2026-09-18 12:40:33 -03:00
|
|
|
&& !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 };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|