Analytics API and Dashboard Data

Updates controllers and view model so the dashboard can load analytics data and expose it through the verification API/page.
main
SYED MUSTUFA AHMED NAQVI 2026-05-15 18:13:47 +05:00
parent ec86ae98e0
commit 2430700dbf
3 changed files with 90 additions and 3 deletions

View File

@ -1,22 +1,39 @@
using System.Diagnostics; using System.Diagnostics;
using AVSCartonShipmentVerifier.Models; using AVSCartonShipmentVerifier.Models;
using AVSCartonShipmentVerifier.Repositories;
using AVSCartonShipmentVerifier.Services; using AVSCartonShipmentVerifier.Services;
using AVSCartonShipmentVerifier.ViewModels; using AVSCartonShipmentVerifier.ViewModels;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace AVSCartonShipmentVerifier.Controllers; namespace AVSCartonShipmentVerifier.Controllers;
public sealed class HomeController(IScanHistoryStore scanHistoryStore) : Controller public sealed class HomeController(
IScanHistoryStore scanHistoryStore,
ICartonVerificationRepository cartonVerificationRepository,
ILogger<HomeController> logger) : Controller
{ {
private readonly IScanHistoryStore _scanHistoryStore = scanHistoryStore; private readonly IScanHistoryStore _scanHistoryStore = scanHistoryStore;
private readonly ICartonVerificationRepository _cartonVerificationRepository = cartonVerificationRepository;
private readonly ILogger<HomeController> _logger = logger;
[HttpGet] [HttpGet]
public async Task<IActionResult> Index(CancellationToken cancellationToken) public async Task<IActionResult> Index(CancellationToken cancellationToken)
{ {
int? availableCartonsCount = null;
try
{
availableCartonsCount = await _cartonVerificationRepository.GetAvailableCartonCountAsync(cancellationToken);
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to load available cartons count for dashboard.");
}
var model = new VerificationDashboardViewModel var model = new VerificationDashboardViewModel
{ {
RecentSuccessfulScans = await _scanHistoryStore.GetLastFiveSuccessfulScansAsync(cancellationToken), RecentSuccessfulScans = await _scanHistoryStore.GetLastFiveSuccessfulScansAsync(cancellationToken),
RecentRejectedScans = await _scanHistoryStore.GetLastFiveRejectedScansAsync(cancellationToken) RecentRejectedScans = await _scanHistoryStore.GetLastFiveRejectedScansAsync(cancellationToken),
AvailableCartonsCount = availableCartonsCount
}; };
return View(model); return View(model);

View File

@ -1,4 +1,5 @@
using AVSCartonShipmentVerifier.DTOs; using AVSCartonShipmentVerifier.DTOs;
using AVSCartonShipmentVerifier.Repositories;
using AVSCartonShipmentVerifier.Services; using AVSCartonShipmentVerifier.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -8,10 +9,14 @@ namespace AVSCartonShipmentVerifier.Controllers;
public sealed class VerificationController( public sealed class VerificationController(
IShipmentVerificationService shipmentVerificationService, IShipmentVerificationService shipmentVerificationService,
IScanHistoryStore scanHistoryStore, IScanHistoryStore scanHistoryStore,
ICartonVerificationRepository cartonVerificationRepository,
IMovementAnalyticsService movementAnalyticsService,
ILogger<VerificationController> logger) : Controller ILogger<VerificationController> logger) : Controller
{ {
private readonly IShipmentVerificationService _shipmentVerificationService = shipmentVerificationService; private readonly IShipmentVerificationService _shipmentVerificationService = shipmentVerificationService;
private readonly IScanHistoryStore _scanHistoryStore = scanHistoryStore; private readonly IScanHistoryStore _scanHistoryStore = scanHistoryStore;
private readonly ICartonVerificationRepository _cartonVerificationRepository = cartonVerificationRepository;
private readonly IMovementAnalyticsService _movementAnalyticsService = movementAnalyticsService;
private readonly ILogger<VerificationController> _logger = logger; private readonly ILogger<VerificationController> _logger = logger;
[HttpPost("scan")] [HttpPost("scan")]
@ -30,11 +35,14 @@ public sealed class VerificationController(
request.EntryMethod ?? "Scanner", request.EntryMethod ?? "Scanner",
cancellationToken); cancellationToken);
var availableCartons = await TryGetAvailableCartonCountAsync(cancellationToken);
return Ok(new return Ok(new
{ {
result = verificationResult, result = verificationResult,
recentSuccessfulScans = await _scanHistoryStore.GetLastFiveSuccessfulScansAsync(cancellationToken), recentSuccessfulScans = await _scanHistoryStore.GetLastFiveSuccessfulScansAsync(cancellationToken),
recentRejectedScans = await _scanHistoryStore.GetLastFiveRejectedScansAsync(cancellationToken) recentRejectedScans = await _scanHistoryStore.GetLastFiveRejectedScansAsync(cancellationToken),
availableCartons
}); });
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@ -52,6 +60,52 @@ public sealed class VerificationController(
} }
} }
[HttpGet("dashboard-summary")]
public async Task<IActionResult> GetDashboardSummary(CancellationToken cancellationToken)
{
var count = await TryGetAvailableCartonCountAsync(cancellationToken);
if (count is null)
{
return StatusCode(StatusCodes.Status503ServiceUnavailable, new { message = "Unable to load available cartons count." });
}
return Ok(new { availableCartons = count.Value });
}
[HttpGet("available-cartons")]
public async Task<IActionResult> GetAvailableCartons(CancellationToken cancellationToken)
{
var count = await TryGetAvailableCartonCountAsync(cancellationToken);
if (count is null)
{
return StatusCode(StatusCodes.Status503ServiceUnavailable, new { message = "Unable to load available cartons count." });
}
return Ok(new { availableCartons = count.Value });
}
[HttpGet("movement-analytics")]
public async Task<IActionResult> GetMovementAnalytics(
[FromQuery] DateOnly? from,
[FromQuery] DateOnly? to,
CancellationToken cancellationToken)
{
var today = DateOnly.FromDateTime(DateTime.Now);
var toDate = to ?? today;
var fromDate = from ?? toDate.AddDays(-6);
try
{
var analytics = await _movementAnalyticsService.GetAnalyticsAsync(fromDate, toDate, cancellationToken);
return Ok(analytics);
}
catch (Exception exception)
{
_logger.LogError(exception, "Unexpected error while loading movement analytics.");
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Unable to load movement analytics." });
}
}
/// <summary> /// <summary>
/// Returns all successful or all rejected scan records from SQLite (not limited to five). /// Returns all successful or all rejected scan records from SQLite (not limited to five).
/// </summary> /// </summary>
@ -81,5 +135,18 @@ public sealed class VerificationController(
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Unable to load scan history." }); return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Unable to load scan history." });
} }
} }
private async Task<int?> TryGetAvailableCartonCountAsync(CancellationToken cancellationToken)
{
try
{
return await _cartonVerificationRepository.GetAvailableCartonCountAsync(cancellationToken);
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to load available cartons count from verification database.");
return null;
}
}
} }

View File

@ -7,5 +7,8 @@ public sealed class VerificationDashboardViewModel
public IReadOnlyCollection<ScannedRecordDto> RecentSuccessfulScans { get; init; } = []; public IReadOnlyCollection<ScannedRecordDto> RecentSuccessfulScans { get; init; } = [];
public IReadOnlyCollection<RejectedScanRecordDto> RecentRejectedScans { get; init; } = []; public IReadOnlyCollection<RejectedScanRecordDto> RecentRejectedScans { get; init; } = [];
/// <summary>Cartons with moved_to_warehouse = 0; null when MySQL count could not be loaded.</summary>
public int? AvailableCartonsCount { get; init; }
} }