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 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 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 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 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 PageAsync( IQueryable 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 }; } } }