mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 16:19:07 +00:00
feat(dashboard): add dispatcher performance (SH-347)
This commit is contained in:
parent
9b39d0e5ec
commit
0194748e3b
7 changed files with 172 additions and 5 deletions
|
|
@ -237,5 +237,68 @@ public class DashboardServiceTests
|
||||||
adminResult.TotalDispatchers.Should().Be(2);
|
adminResult.TotalDispatchers.Should().Be(2);
|
||||||
adminResult.Items.Select(item => item.DispatcherName)
|
adminResult.Items.Select(item => item.DispatcherName)
|
||||||
.Should().ContainInOrder("Ada One", "Ben Two");
|
.Should().ContainInOrder("Ada One", "Ben Two");
|
||||||
|
|
||||||
|
var performance = await NewService(ctx).GetPerformanceAsync(
|
||||||
|
AccountUser(1),
|
||||||
|
new DashboardStatsQueryDTO(),
|
||||||
|
1,
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
performance.TotalDispatchers.Should().Be(2);
|
||||||
|
performance.Items[0].DispatcherName.Should().Be("Ada One");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPerformanceAsync_UsesDueOnlyRateAndOriginalDateResolution()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
ctx.Roles.Add(new IdentityRole
|
||||||
|
{
|
||||||
|
Id = "dispatcher-role",
|
||||||
|
Name = "Dispatcher",
|
||||||
|
NormalizedName = "DISPATCHER"
|
||||||
|
});
|
||||||
|
ctx.Users.Add(new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = "dispatcher-1",
|
||||||
|
FirstName = "Ada",
|
||||||
|
LastName = "One",
|
||||||
|
Color = "#123456"
|
||||||
|
});
|
||||||
|
ctx.UserRoles.Add(new IdentityUserRole<string>
|
||||||
|
{
|
||||||
|
UserId = "dispatcher-1",
|
||||||
|
RoleId = "dispatcher-role"
|
||||||
|
});
|
||||||
|
var today = DashboardBusinessTime.Today();
|
||||||
|
ctx.workOrders.AddRange(
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
AccountId = 1,
|
||||||
|
AssignTo = "dispatcher-1",
|
||||||
|
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Completed,
|
||||||
|
ScheduledDate = today.ToDateTime(TimeOnly.MinValue),
|
||||||
|
OriginalDate = today.AddDays(-5),
|
||||||
|
CompletedDate = today.AddDays(-2).ToDateTime(TimeOnly.MinValue)
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
AccountId = 1,
|
||||||
|
AssignTo = "dispatcher-1",
|
||||||
|
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Scheduled,
|
||||||
|
ScheduledDate = today.AddDays(3).ToDateTime(TimeOnly.MinValue)
|
||||||
|
});
|
||||||
|
ctx.SaveChanges();
|
||||||
|
|
||||||
|
var result = await NewService(ctx).GetPerformanceAsync(
|
||||||
|
AccountUser(1),
|
||||||
|
new DashboardStatsQueryDTO(),
|
||||||
|
1,
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
result.Items.Should().ContainSingle();
|
||||||
|
result.Items[0].CompletionRate.Should().Be(100m);
|
||||||
|
result.Items[0].AverageResolutionDays.Should().Be(3m);
|
||||||
|
result.Items[0].Color.Should().Be("#123456");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,5 +63,31 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
return BadRequest(ex.Message);
|
return BadRequest(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("Performance")]
|
||||||
|
public async Task<IActionResult> GetPerformance(
|
||||||
|
[FromQuery] DashboardStatsQueryDTO query,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Ok(await _dashboardService.GetPerformanceAsync(
|
||||||
|
User, query, page, cancellationToken));
|
||||||
|
}
|
||||||
|
catch (SeaHaven.Services.Exceptions.WorkOrderBoardValidationException ex)
|
||||||
|
when (ex.Code == "Forbidden")
|
||||||
|
{
|
||||||
|
return StatusCode(StatusCodes.Status403Forbidden, new
|
||||||
|
{
|
||||||
|
code = ex.Code,
|
||||||
|
message = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,8 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
select new
|
select new
|
||||||
{
|
{
|
||||||
user.Id,
|
user.Id,
|
||||||
Name = ((user.FirstName ?? "") + " " + (user.LastName ?? "")).Trim()
|
Name = ((user.FirstName ?? "") + " " + (user.LastName ?? "")).Trim(),
|
||||||
|
user.Color
|
||||||
};
|
};
|
||||||
|
|
||||||
var workOrders = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
var workOrders = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
||||||
|
|
@ -140,10 +141,11 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
{
|
{
|
||||||
var count = matchingCounts.SingleOrDefault();
|
var count = matchingCounts.SingleOrDefault();
|
||||||
return new DashboardDispatcherWorkload(
|
return new DashboardDispatcherWorkload(
|
||||||
user.Id,
|
user.Id,
|
||||||
user.Name,
|
user.Name,
|
||||||
count?.TotalCount ?? 0,
|
user.Color,
|
||||||
count?.OpenCount ?? 0);
|
count?.TotalCount ?? 0,
|
||||||
|
count?.OpenCount ?? 0);
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
public sealed record DashboardDispatcherWorkload(
|
public sealed record DashboardDispatcherWorkload(
|
||||||
string DispatcherId,
|
string DispatcherId,
|
||||||
string DispatcherName,
|
string DispatcherName,
|
||||||
|
string? Color,
|
||||||
int TotalCount,
|
int TotalCount,
|
||||||
int OpenCount);
|
int OpenCount);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,28 @@ namespace SeaHaven.Services.DTOs
|
||||||
{
|
{
|
||||||
public string DispatcherId { get; init; } = string.Empty;
|
public string DispatcherId { get; init; } = string.Empty;
|
||||||
public string DispatcherName { get; init; } = string.Empty;
|
public string DispatcherName { get; init; } = string.Empty;
|
||||||
|
public string? Color { get; init; }
|
||||||
public int TotalCount { get; init; }
|
public int TotalCount { get; init; }
|
||||||
public int OpenCount { get; init; }
|
public int OpenCount { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class DashboardPerformanceResponseDTO
|
||||||
|
{
|
||||||
|
public IReadOnlyList<DashboardPerformanceRowDTO> Items { get; init; } = [];
|
||||||
|
public int Page { get; init; }
|
||||||
|
public int PageSize { get; init; }
|
||||||
|
public int TotalDispatchers { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DashboardPerformanceRowDTO
|
||||||
|
{
|
||||||
|
public string DispatcherId { get; init; } = string.Empty;
|
||||||
|
public string DispatcherName { get; init; } = string.Empty;
|
||||||
|
public string? Color { get; init; }
|
||||||
|
public decimal CompletionRate { get; init; }
|
||||||
|
public decimal? AverageResolutionDays { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public class DashboardStatsDTO
|
public class DashboardStatsDTO
|
||||||
{
|
{
|
||||||
public int Total { get; set; }
|
public int Total { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
{
|
{
|
||||||
DispatcherId = row.DispatcherId,
|
DispatcherId = row.DispatcherId,
|
||||||
DispatcherName = row.DispatcherName,
|
DispatcherName = row.DispatcherName,
|
||||||
|
Color = row.Color,
|
||||||
TotalCount = row.TotalCount,
|
TotalCount = row.TotalCount,
|
||||||
OpenCount = row.OpenCount
|
OpenCount = row.OpenCount
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
|
|
@ -101,5 +102,55 @@ namespace SeaHaven.Services.Implementation
|
||||||
TotalDispatchers = orderedRows.Count
|
TotalDispatchers = orderedRows.Count
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DashboardPerformanceResponseDTO> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,11 @@ namespace SeaHaven.Services.Interfaces
|
||||||
DashboardStatsQueryDTO query,
|
DashboardStatsQueryDTO query,
|
||||||
int page,
|
int page,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<DashboardPerformanceResponseDTO> GetPerformanceAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
DashboardStatsQueryDTO query,
|
||||||
|
int page,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue