mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 10:43:13 +00:00
Adds GET /api/notifications, a per-user read model derived from live work-order state: Unassigned (grouped, High), No Vendor and Aveta Missing (per work order, Medium) and Vendor Conflict, account-scoped from claims and ordered by section severity with a fixed reason tie-break.
112 lines
5.2 KiB
C#
112 lines
5.2 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 non-terminal dispatch that
|
|
// points at the work order directly or links it through DispatchWorkOrders.
|
|
var candidates = OpenWorkOrders(scope)
|
|
.Where(w => w.ScheduledDate.HasValue
|
|
&& w.ScheduledDate.Value <= cutoff
|
|
&& !_context.Dispatches.Any(d => d.WorkOrderId == w.Id
|
|
&& !TerminalDispatchStatuses.Contains(d.Status ?? ""))
|
|
&& !_context.DispatchWorkOrders.Any(link => link.WorkOrderId == w.Id
|
|
&& link.Dispatch != null
|
|
&& !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 };
|
|
}
|
|
}
|
|
}
|