AVS-Carton-Shipment-Verifier/Services/MovementAnalyticsService.cs

175 lines
7.3 KiB
C#

using AVSCartonShipmentVerifier.DTOs;
using AVSCartonShipmentVerifier.Repositories;
using AVSCartonShipmentVerifier.ScanHistory;
using Microsoft.EntityFrameworkCore;
namespace AVSCartonShipmentVerifier.Services;
public sealed class MovementAnalyticsService(
ScanHistoryDbContext dbContext,
ICartonVerificationRepository cartonVerificationRepository,
ILogger<MovementAnalyticsService> logger) : IMovementAnalyticsService
{
private readonly ScanHistoryDbContext _dbContext = dbContext;
private readonly ICartonVerificationRepository _cartonVerificationRepository = cartonVerificationRepository;
private readonly ILogger<MovementAnalyticsService> _logger = logger;
public async Task<MovementAnalyticsDto> GetAnalyticsAsync(DateOnly fromDate, DateOnly toDate, CancellationToken cancellationToken = default)
{
if (toDate < fromDate)
{
(fromDate, toDate) = (toDate, fromDate);
}
var today = DateOnly.FromDateTime(DateTime.Now);
var (todayStartUtc, todayEndUtc) = GetUtcDayRange(today);
var (weekStartUtc, weekEndUtc) = GetUtcWeekRange(today);
var (monthStartUtc, monthEndUtc) = GetUtcMonthRange(today);
var (rangeStartUtc, rangeEndUtc) = GetUtcDayRange(fromDate, toDate);
int movedToday;
int movedThisWeek;
int movedThisMonth;
int totalMoved;
int pending;
List<ScanHistoryEntry> successfulInRange;
List<RejectedScanEntry> rejectedInRange;
try
{
movedToday = await CountSuccessfulScansAsync(todayStartUtc, todayEndUtc, cancellationToken);
movedThisWeek = await CountSuccessfulScansAsync(weekStartUtc, weekEndUtc, cancellationToken);
movedThisMonth = await CountSuccessfulScansAsync(monthStartUtc, monthEndUtc, cancellationToken);
totalMoved = await _cartonVerificationRepository.GetTotalMovedCartonCountAsync(cancellationToken);
pending = await _cartonVerificationRepository.GetAvailableCartonCountAsync(cancellationToken);
successfulInRange = await _dbContext.ScanHistoryEntries
.AsNoTracking()
.Where(x => x.ShipmentMarked && x.ProcessedAtUtc >= rangeStartUtc && x.ProcessedAtUtc <= rangeEndUtc)
.ToListAsync(cancellationToken);
rejectedInRange = await _dbContext.RejectedScanEntries
.AsNoTracking()
.Where(x => x.ProcessedAtUtc >= rangeStartUtc && x.ProcessedAtUtc <= rangeEndUtc)
.ToListAsync(cancellationToken);
}
catch (OperationCanceledException exception) when (cancellationToken.IsCancellationRequested)
{
_logger.LogDebug(exception, "Movement analytics read was cancelled.");
throw;
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to load movement analytics.");
throw;
}
var successfulByLocalDate = successfulInRange
.GroupBy(x => ToLocalDateOnly(x.ProcessedAtUtc))
.ToDictionary(g => g.Key, g => g.ToList());
var rejectedByLocalDate = rejectedInRange
.GroupBy(x => ToLocalDateOnly(x.ProcessedAtUtc))
.ToDictionary(g => g.Key, g => g.ToList());
var chartByDay = new List<MovementChartDayDto>();
var dayDetails = new List<MovementDayDetailDto>();
for (var date = fromDate; date <= toDate; date = date.AddDays(1))
{
successfulByLocalDate.TryGetValue(date, out var daySuccessful);
daySuccessful ??= [];
rejectedByLocalDate.TryGetValue(date, out var dayRejected);
dayRejected ??= [];
var movedCount = daySuccessful.Count;
var duplicateCount = dayRejected.Count(r =>
string.Equals(r.RejectionReason, VerificationRejectionReasons.AlreadyTransferredDuplicateScan, StringComparison.Ordinal));
var manualCount = dayRejected.Count(r =>
string.Equals(r.RejectionReason, VerificationRejectionReasons.ManualEntryNotAllowed, StringComparison.Ordinal));
DateTime? lastMovedUtc = daySuccessful.Count > 0
? daySuccessful.Max(x => x.ProcessedAtUtc)
: null;
var daySpan = toDate.DayNumber - fromDate.DayNumber + 1;
var dayLabel = daySpan <= 7
? date.ToString("ddd", System.Globalization.CultureInfo.InvariantCulture)
: date.ToString("MMM d", System.Globalization.CultureInfo.InvariantCulture);
chartByDay.Add(new MovementChartDayDto
{
DayLabel = dayLabel,
MovedCartons = movedCount
});
dayDetails.Add(new MovementDayDetailDto
{
Date = date,
DateDisplay = $"{date.Day} {date:MMM yyyy} ({date:ddd})",
MovedCartons = movedCount,
SuccessfulScans = movedCount,
DuplicateRejections = duplicateCount,
ManualEntryRejections = manualCount,
LastMovedAtUtc = lastMovedUtc
});
}
dayDetails.Reverse();
return new MovementAnalyticsDto
{
Summary = new MovementSummaryDto
{
MovedToday = movedToday,
MovedThisWeek = movedThisWeek,
MovedThisMonth = movedThisMonth,
TotalMoved = totalMoved
},
StatusShare = new MovementStatusShareDto
{
Moved = totalMoved,
Pending = pending
},
ChartByDay = chartByDay,
DayDetails = dayDetails,
DateRangeLabel = $"{fromDate:MMM d, yyyy} - {toDate:MMM d, yyyy}"
};
}
private async Task<int> CountSuccessfulScansAsync(DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken) =>
await _dbContext.ScanHistoryEntries
.AsNoTracking()
.CountAsync(x => x.ShipmentMarked && x.ProcessedAtUtc >= startUtc && x.ProcessedAtUtc <= endUtc, cancellationToken);
private static DateOnly ToLocalDateOnly(DateTime utc) =>
DateOnly.FromDateTime(DateTime.SpecifyKind(utc, DateTimeKind.Utc).ToLocalTime());
private static (DateTime StartUtc, DateTime EndUtc) GetUtcDayRange(DateOnly date) =>
GetUtcDayRange(date, date);
private static (DateTime StartUtc, DateTime EndUtc) GetUtcDayRange(DateOnly from, DateOnly to)
{
var startLocal = from.ToDateTime(TimeOnly.MinValue);
var endLocal = to.ToDateTime(new TimeOnly(23, 59, 59, 999));
return (TimeZoneInfo.ConvertTimeToUtc(startLocal), TimeZoneInfo.ConvertTimeToUtc(endLocal));
}
private static (DateTime StartUtc, DateTime EndUtc) GetUtcWeekRange(DateOnly anchor)
{
var local = anchor.ToDateTime(TimeOnly.MinValue);
var diff = (7 + (local.DayOfWeek - DayOfWeek.Monday)) % 7;
var weekStart = local.AddDays(-diff);
var weekEnd = weekStart.AddDays(6);
return GetUtcDayRange(DateOnly.FromDateTime(weekStart), DateOnly.FromDateTime(weekEnd));
}
private static (DateTime StartUtc, DateTime EndUtc) GetUtcMonthRange(DateOnly anchor)
{
var monthStart = new DateOnly(anchor.Year, anchor.Month, 1);
var monthEnd = monthStart.AddMonths(1).AddDays(-1);
return GetUtcDayRange(monthStart, monthEnd);
}
}