mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 09:33: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");
|
||||
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.OriginalDate,
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,12 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
DateTime? ScheduledDate,
|
||||
DateOnly? OriginalDate,
|
||||
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(
|
||||
string DispatcherId,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,24 @@ namespace SeaHaven.Services.DTOs
|
|||
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 int Total { get; set; }
|
||||
|
|
|
|||
|
|
@ -179,5 +179,54 @@ namespace SeaHaven.Services.Implementation
|
|||
.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,
|
||||
DashboardStatsQueryDTO query,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<DashboardVendorInsightsResponseDTO> GetVendorInsightsAsync(
|
||||
ClaimsPrincipal user,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue