170 lines
7.1 KiB
C#
170 lines
7.1 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 (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);
|
|
}
|
|
}
|