using System.Security.Claims; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Helpers; using SeaHaven.Services.Interfaces; namespace SeaHaven.Services.Implementation { public class DashboardService : IDashboardService { private readonly IDashboardDataService _dataService; private readonly IWorkOrderAccountResolver _accountResolver; public DashboardService( IDashboardDataService dataService, IWorkOrderAccountResolver accountResolver) { _dataService = dataService; _accountResolver = accountResolver; } public async Task GetStatsAsync( ClaimsPrincipal user, DashboardStatsQueryDTO query, CancellationToken cancellationToken) { var accountId = _accountResolver.ResolveAccountFilter(user); var dispatcherId = user.IsInRole("Dispatcher") ? user.FindFirstValue(ClaimTypes.NameIdentifier) : null; if (user.IsInRole("Dispatcher") && string.IsNullOrWhiteSpace(dispatcherId)) throw new WorkOrderBoardValidationException("Forbidden", "Dispatcher identity is required."); var counts = await _dataService.GetWorkOrderCountsAsync(accountId, cancellationToken); var workOrders = await _dataService.GetDashboardWorkOrdersAsync( accountId, dispatcherId, query.DateFrom, query.DateTo, cancellationToken); var metrics = DashboardMetrics.Calculate(workOrders, DashboardBusinessTime.Today()); var useMetrics = query.DateFrom.HasValue && query.DateTo.HasValue || dispatcherId is not null; return new DashboardStatsDTO { Total = useMetrics ? metrics.Total : counts.Total, Open = useMetrics ? metrics.Open : counts.Open, NotDispatched = useMetrics ? metrics.NotDispatched : counts.NotDispatched, Completed = useMetrics ? metrics.Completed : counts.Completed, Breakdown = metrics.Breakdown, DueCount = metrics.DueCount, CompletedDueCount = metrics.CompletedDueCount, CompletionRate = metrics.CompletionRate, AverageResolutionDays = metrics.AverageResolutionDays }; } public async Task GetWorkloadAsync( ClaimsPrincipal user, DashboardStatsQueryDTO query, int page, CancellationToken cancellationToken) { if (page < 1) throw new ArgumentOutOfRangeException(nameof(page)); var accountId = _accountResolver.ResolveAccountFilter(user); var dispatcherId = user.IsInRole("Dispatcher") ? user.FindFirstValue(ClaimTypes.NameIdentifier) : null; if (user.IsInRole("Dispatcher") && string.IsNullOrWhiteSpace(dispatcherId)) throw new WorkOrderBoardValidationException("Forbidden", "Dispatcher identity is required."); var rows = await _dataService.GetDispatcherWorkloadAsync( accountId, dispatcherId, query.DateFrom, query.DateTo, cancellationToken); var orderedRows = rows .OrderByDescending(row => row.OpenCount) .ThenBy(row => row.DispatcherName, StringComparer.OrdinalIgnoreCase) .ThenBy(row => row.DispatcherId, StringComparer.Ordinal) .ToList(); const int pageSize = 10; return new DashboardWorkloadResponseDTO { Items = orderedRows.Skip((page - 1) * pageSize).Take(pageSize) .Select(row => new DashboardWorkloadRowDTO { DispatcherId = row.DispatcherId, DispatcherName = row.DispatcherName, Color = row.Color, TotalCount = row.TotalCount, OpenCount = row.OpenCount }).ToList(), Page = page, PageSize = pageSize, TotalDispatchers = orderedRows.Count }; } public async Task GetPerformanceAsync( ClaimsPrincipal user, DashboardStatsQueryDTO query, int page, CancellationToken cancellationToken) { if (page < 1) throw new ArgumentOutOfRangeException(nameof(page)); var accountId = _accountResolver.ResolveAccountFilter(user); var dispatcherId = user.IsInRole("Dispatcher") ? user.FindFirstValue(ClaimTypes.NameIdentifier) : null; if (user.IsInRole("Dispatcher") && string.IsNullOrWhiteSpace(dispatcherId)) throw new WorkOrderBoardValidationException("Forbidden", "Dispatcher identity is required."); var directory = await _dataService.GetDispatcherWorkloadAsync( accountId, dispatcherId, query.DateFrom, query.DateTo, cancellationToken); var workOrders = await _dataService.GetDashboardWorkOrdersAsync( accountId, dispatcherId, query.DateFrom, query.DateTo, cancellationToken); var today = DashboardBusinessTime.Today(); var rows = directory.Select(dispatcher => { var metrics = DashboardMetrics.Calculate( workOrders.Where(workOrder => workOrder.AssignTo == dispatcher.DispatcherId).ToList(), today); return new DashboardPerformanceRowDTO { DispatcherId = dispatcher.DispatcherId, DispatcherName = dispatcher.DispatcherName, Color = dispatcher.Color, CompletionRate = metrics.CompletionRate, AverageResolutionDays = metrics.AverageResolutionDays }; }) .OrderByDescending(row => row.CompletionRate) .ThenBy(row => row.DispatcherName, StringComparer.OrdinalIgnoreCase) .ThenBy(row => row.DispatcherId, StringComparer.Ordinal) .ToList(); const int pageSize = 10; return new DashboardPerformanceResponseDTO { Items = rows.Skip((page - 1) * pageSize).Take(pageSize).ToList(), Page = page, PageSize = pageSize, TotalDispatchers = rows.Count }; } public async Task GetRegionsAsync( ClaimsPrincipal user, DashboardStatsQueryDTO query, CancellationToken cancellationToken) { var accountId = _accountResolver.ResolveAccountFilter(user); var dispatcherId = user.IsInRole("Dispatcher") ? user.FindFirstValue(ClaimTypes.NameIdentifier) : null; if (user.IsInRole("Dispatcher") && string.IsNullOrWhiteSpace(dispatcherId)) throw new WorkOrderBoardValidationException("Forbidden", "Dispatcher identity is required."); var workOrders = await _dataService.GetDashboardWorkOrdersAsync( accountId, dispatcherId, query.DateFrom, query.DateTo, cancellationToken); var counts = DashboardRegions.Count(workOrders); return new DashboardRegionResponseDTO { Items = DashboardRegions.Names .Select(region => new DashboardRegionRowDTO { Region = region, WorkOrderCount = counts[region] }) .ToList() }; } public async Task GetVendorInsightsAsync( ClaimsPrincipal user, CancellationToken cancellationToken) { var accountId = _accountResolver.ResolveAccountFilter(user); var workOrders = await _dataService.GetDashboardWorkOrdersAsync( accountId, null, null, null, cancellationToken); var activeVendorOrders = workOrders .Where(workOrder => workOrder.VendorCompanyId.HasValue && workOrder.VendorIsActive && !workOrder.VendorCompanyIsDeleted) .ToList(); var today = DashboardBusinessTime.Today(); var rows = activeVendorOrders .GroupBy(workOrder => new { Id = workOrder.VendorCompanyId!.Value, Name = workOrder.VendorCompanyName ?? string.Empty }) .Select(group => { var orders = group.ToList(); var metrics = DashboardMetrics.Calculate(orders, today); var rescheduled = orders.Count(order => order.RescheduleCount >= 1); return new DashboardVendorInsightsRowDTO { VendorCompanyId = group.Key.Id, VendorCompanyName = group.Key.Name, CompletionRate = metrics.CompletionRate, RescheduleRate = Math.Round((decimal)rescheduled / orders.Count * 100, 2), AverageResolutionDays = metrics.AverageResolutionDays, TotalJobs = orders.Count }; }) .OrderByDescending(row => row.CompletionRate) .ThenBy(row => row.VendorCompanyName, StringComparer.OrdinalIgnoreCase) .ThenBy(row => row.VendorCompanyId) .ToList(); const int pageSize = 10; return new DashboardVendorInsightsResponseDTO { Items = rows.Take(pageSize).ToList(), Page = 1, PageSize = pageSize, TotalVendors = rows.Count }; } } }