mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-05 06:02:13 +00:00
feat(dashboard): add vendor insights (SH-349)
This commit is contained in:
parent
d4afcced87
commit
3358ea5058
7 changed files with 152 additions and 2 deletions
|
|
@ -324,4 +324,47 @@ public class DashboardServiceTests
|
||||||
"East", "Central", "West", "California", "Unmapped/Other");
|
"East", "Central", "West", "California", "Unmapped/Other");
|
||||||
result.Items.Select(row => row.WorkOrderCount).Should().Equal(1, 0, 0, 1, 2);
|
result.Items.Select(row => row.WorkOrderCount).Should().Equal(1, 0, 0, 1, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetVendorInsightsAsync_IsCompanyWideAndUsesVendorMetrics()
|
||||||
|
{
|
||||||
|
await using var ctx = NewContext();
|
||||||
|
ctx.VendorCompanies.Add(new VendorCompany { Id = 10, Name = "Acme Services", IsDeleted = false });
|
||||||
|
ctx.Vendors.Add(new Vendor
|
||||||
|
{
|
||||||
|
Id = 20,
|
||||||
|
CompanyId = 10,
|
||||||
|
IsActive = true,
|
||||||
|
CompanyName = "Acme Services"
|
||||||
|
});
|
||||||
|
ctx.Dispatches.Add(new Dispatch { Id = 30, VendorId = 20 });
|
||||||
|
ctx.workOrders.AddRange(
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
AccountId = 1,
|
||||||
|
PrimaryDispatchId = 30,
|
||||||
|
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Completed,
|
||||||
|
ScheduledDate = DateTime.UtcNow.Date.AddDays(-3),
|
||||||
|
CompletedDate = DateTime.UtcNow.Date.AddDays(-1),
|
||||||
|
RescheduleCount = 1
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
AccountId = 1,
|
||||||
|
PrimaryDispatchId = 30,
|
||||||
|
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Completed,
|
||||||
|
ScheduledDate = DateTime.UtcNow.Date.AddDays(-2),
|
||||||
|
CompletedDate = DateTime.UtcNow.Date.AddDays(-1)
|
||||||
|
});
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await NewService(ctx).GetVendorInsightsAsync(
|
||||||
|
AccountUser(1), CancellationToken.None);
|
||||||
|
|
||||||
|
result.TotalVendors.Should().Be(1);
|
||||||
|
result.Items.Should().ContainSingle();
|
||||||
|
result.Items[0].TotalJobs.Should().Be(2);
|
||||||
|
result.Items[0].CompletionRate.Should().Be(100);
|
||||||
|
result.Items[0].RescheduleRate.Should().Be(50);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,5 +109,23 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("VendorInsights")]
|
||||||
|
public async Task<IActionResult> GetVendorInsights(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Ok(await _dashboardService.GetVendorInsightsAsync(User, cancellationToken));
|
||||||
|
}
|
||||||
|
catch (SeaHaven.Services.Exceptions.WorkOrderBoardValidationException ex)
|
||||||
|
when (ex.Code == "Forbidden")
|
||||||
|
{
|
||||||
|
return StatusCode(StatusCodes.Status403Forbidden, new
|
||||||
|
{
|
||||||
|
code = ex.Code,
|
||||||
|
message = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,20 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
w.ScheduledDate,
|
w.ScheduledDate,
|
||||||
w.OriginalDate,
|
w.OriginalDate,
|
||||||
w.CompletedDate,
|
w.CompletedDate,
|
||||||
w.Locations == null ? null : w.Locations.State))
|
w.Locations == null ? null : w.Locations.State,
|
||||||
|
w.RescheduleCount,
|
||||||
|
w.PrimaryDispatch == null || w.PrimaryDispatch.Vendor == null
|
||||||
|
? null
|
||||||
|
: w.PrimaryDispatch.Vendor.CompanyId,
|
||||||
|
w.PrimaryDispatch == null || w.PrimaryDispatch.Vendor == null
|
||||||
|
|| w.PrimaryDispatch.Vendor.Company == null
|
||||||
|
? null
|
||||||
|
: w.PrimaryDispatch.Vendor.Company.Name,
|
||||||
|
w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null
|
||||||
|
&& w.PrimaryDispatch.Vendor.IsActive,
|
||||||
|
w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null
|
||||||
|
&& w.PrimaryDispatch.Vendor.Company != null
|
||||||
|
&& w.PrimaryDispatch.Vendor.Company.IsDeleted == true))
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,12 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
DateTime? ScheduledDate,
|
DateTime? ScheduledDate,
|
||||||
DateOnly? OriginalDate,
|
DateOnly? OriginalDate,
|
||||||
DateTime? CompletedDate,
|
DateTime? CompletedDate,
|
||||||
string? LocationState = null);
|
string? LocationState = null,
|
||||||
|
int RescheduleCount = 0,
|
||||||
|
int? VendorCompanyId = null,
|
||||||
|
string? VendorCompanyName = null,
|
||||||
|
bool VendorIsActive = false,
|
||||||
|
bool VendorCompanyIsDeleted = false);
|
||||||
|
|
||||||
public sealed record DashboardDispatcherWorkload(
|
public sealed record DashboardDispatcherWorkload(
|
||||||
string DispatcherId,
|
string DispatcherId,
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,24 @@ namespace SeaHaven.Services.DTOs
|
||||||
public int WorkOrderCount { get; init; }
|
public int WorkOrderCount { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class DashboardVendorInsightsResponseDTO
|
||||||
|
{
|
||||||
|
public IReadOnlyList<DashboardVendorInsightsRowDTO> Items { get; init; } = [];
|
||||||
|
public int Page { get; init; }
|
||||||
|
public int PageSize { get; init; }
|
||||||
|
public int TotalVendors { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DashboardVendorInsightsRowDTO
|
||||||
|
{
|
||||||
|
public int VendorCompanyId { get; init; }
|
||||||
|
public string VendorCompanyName { get; init; } = string.Empty;
|
||||||
|
public decimal CompletionRate { get; init; }
|
||||||
|
public decimal RescheduleRate { get; init; }
|
||||||
|
public decimal? AverageResolutionDays { get; init; }
|
||||||
|
public int TotalJobs { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public class DashboardStatsDTO
|
public class DashboardStatsDTO
|
||||||
{
|
{
|
||||||
public int Total { get; set; }
|
public int Total { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -179,5 +179,54 @@ namespace SeaHaven.Services.Implementation
|
||||||
.ToList()
|
.ToList()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DashboardVendorInsightsResponseDTO> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,9 @@ namespace SeaHaven.Services.Interfaces
|
||||||
ClaimsPrincipal user,
|
ClaimsPrincipal user,
|
||||||
DashboardStatsQueryDTO query,
|
DashboardStatsQueryDTO query,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<DashboardVendorInsightsResponseDTO> GetVendorInsightsAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue