using Microsoft.EntityFrameworkCore; /// /// Provides filtered, sorted work queue views for each workflow stage. /// All queues sort by UpdatedAt ASC to enforce FIFO processing — the oldest /// pending item is always at the top of the queue. /// public class WorkQueueService : IWorkQueueService { private readonly AppDbContext _db; private readonly ILogger _logger; public WorkQueueService(AppDbContext db, ILogger logger) { _db = db; _logger = logger; } public async Task GetVerificationQueueAsync(int page, int pageSize) { var query = _db.DigitizationBatches .Where(b => b.Status == BatchStatus.PendingVerification) .OrderBy(b => b.UpdatedAt); return await BuildQueueResponseAsync("verification", query, page, pageSize); } public async Task GetEntryQueueAsync(int page, int pageSize) { var entryStatuses = new[] { BatchStatus.Uploaded, BatchStatus.InEntry, BatchStatus.Rejected }; var query = _db.DigitizationBatches .Where(b => entryStatuses.Contains(b.Status)) .OrderBy(b => b.UpdatedAt); return await BuildQueueResponseAsync("entry", query, page, pageSize); } public async Task GetClinicalApprovalQueueAsync(int page, int pageSize) { var query = _db.DigitizationBatches .Where(b => b.Status == BatchStatus.AwaitingClinicalApproval) .OrderBy(b => b.UpdatedAt); return await BuildQueueResponseAsync("clinical-approval", query, page, pageSize); } private async Task BuildQueueResponseAsync( string queueName, IOrderedQueryable query, int page, int pageSize) { var totalCount = await query.CountAsync(); var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); var items = await query .Include(b => b.EnteredByUser) .Include(b => b.Events) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(b => new WorkQueueItemResponse( b.Id, b.Status.ToDbString(), b.BatchType.ToDbString(), b.Track.ToDbString(), b.PatientId, b.EnteredByUserId, b.EnteredByUser != null ? b.EnteredByUser.FullName : null, b.RejectionReason, b.CreatedAt, b.UpdatedAt, b.Events.Count )) .ToListAsync(); return new WorkQueueResponse(queueName, items, page, pageSize, totalCount, totalPages); } public async Task GetOverviewAsync() { var now = DateTimeOffset.UtcNow; var statusGroups = await _db.DigitizationBatches .AsNoTracking() .GroupBy(b => b.Status) .Select(g => new { Status = g.Key, Count = g.Count() }) .ToListAsync(); var statusCounts = new Dictionary(); foreach (var status in Enum.GetValues()) { var count = statusGroups.FirstOrDefault(g => g.Status == status)?.Count ?? 0; statusCounts[status.ToDbString()] = count; } var pendingBatches = await _db.DigitizationBatches .AsNoTracking() .Where(b => b.Status == BatchStatus.PendingVerification) .Select(b => b.UpdatedAt) .ToListAsync(); double avgTimeInQueueMinutes = 0; double oldestPendingMinutes = 0; if (pendingBatches.Count > 0) { var ages = pendingBatches .Select(updatedAt => (now - updatedAt).TotalMinutes) .ToList(); avgTimeInQueueMinutes = ages.Average(); oldestPendingMinutes = ages.Max(); } var cutoff = now.AddHours(-24); var recentEvents = await _db.DigitizationEvents .AsNoTracking() .Where(e => e.OccurredAt >= cutoff) .Where(e => e.EventType == DigitizationEventType.Rejected || e.EventType == DigitizationEventType.Verified || e.EventType == DigitizationEventType.VerifiedPendingClinical) .GroupBy(e => e.EventType) .Select(g => new { EventType = g.Key, Count = g.Count() }) .ToListAsync(); var rejections = recentEvents .Where(e => e.EventType == DigitizationEventType.Rejected) .Sum(e => e.Count); var verifications = recentEvents .Where(e => e.EventType == DigitizationEventType.Verified || e.EventType == DigitizationEventType.VerifiedPendingClinical) .Sum(e => e.Count); var totalDecisions = rejections + verifications; var rejectRate = totalDecisions > 0 ? (double)rejections / totalDecisions : 0.0; _logger.LogDebug( "Work queue overview: {PendingCount} pending, avg queue {AvgMinutes:F1}m, reject rate {RejectRate:P1}", pendingBatches.Count, avgTimeInQueueMinutes, rejectRate); return new WorkQueueOverviewResponse( StatusCounts: statusCounts, AverageTimeInQueueMinutes: Math.Round(avgTimeInQueueMinutes, 1), RejectRate: Math.Round(rejectRate, 4), OldestPendingVerificationMinutes: Math.Round(oldestPendingMinutes, 1)); } }