mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 16:19:07 +00:00
fix(work-orders): use dedicated overdue query flag
Stop treating WorkOrderType.Other as an overdue sentinel. Board and advanced search now accept overdue=true while types=Other filters real Other rows; combining both uses OR.
This commit is contained in:
parent
188b4f0c58
commit
f6c7c1ec7f
13 changed files with 134 additions and 12 deletions
|
|
@ -112,6 +112,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[FromQuery] List<string>? dispatchers = null,
|
||||
[FromQuery] bool myWorkOrders = false,
|
||||
[FromQuery] List<WorkOrderType>? types = null,
|
||||
[FromQuery] bool overdue = false,
|
||||
[FromQuery] string? search = null)
|
||||
{
|
||||
try
|
||||
|
|
@ -124,6 +125,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
Dispatchers = dispatchers,
|
||||
MyWorkOrders = myWorkOrders,
|
||||
Types = types,
|
||||
Overdue = overdue,
|
||||
Search = search
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -38,22 +38,25 @@ namespace SeaHaven.DataServices.Helpers
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// FE sends <see cref="WorkOrderType.Other"/> (99) as the overdue/isPastDue sentinel — not a real type filter.
|
||||
/// Filters by real <paramref name="types"/> (including <see cref="WorkOrderType.Other"/>)
|
||||
/// and/or past-due rows when <paramref name="overdue"/> is true.
|
||||
/// Types and overdue are combined with OR when both are present.
|
||||
/// </summary>
|
||||
public static IQueryable<WorkOrder> ApplyTypeAndOverdueFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<WorkOrderType>? types,
|
||||
bool overdue = false,
|
||||
DateTime? utcNow = null)
|
||||
{
|
||||
if (types == null || types.Count == 0)
|
||||
var hasTypes = types != null && types.Count > 0;
|
||||
if (!hasTypes && !overdue)
|
||||
return query;
|
||||
|
||||
var today = (utcNow ?? DateTime.UtcNow).Date;
|
||||
var includeOverdue = types.Contains(WorkOrderType.Other);
|
||||
var typeFilters = types.Where(t => t != WorkOrderType.Other).ToList();
|
||||
var typeFilters = hasTypes ? types!.ToList() : new List<WorkOrderType>();
|
||||
|
||||
// Keep past-due predicate inline — EF cannot translate local method calls.
|
||||
if (includeOverdue && typeFilters.Count == 0)
|
||||
if (overdue && !hasTypes)
|
||||
{
|
||||
return query.Where(w =>
|
||||
w.ScheduledDate != null
|
||||
|
|
@ -62,7 +65,7 @@ namespace SeaHaven.DataServices.Helpers
|
|||
&& w.LifecycleStatus != LifecycleStatus.Canceled);
|
||||
}
|
||||
|
||||
if (includeOverdue)
|
||||
if (overdue)
|
||||
{
|
||||
return query.Where(w =>
|
||||
(w.ScheduledDate != null
|
||||
|
|
@ -77,8 +80,9 @@ namespace SeaHaven.DataServices.Helpers
|
|||
|
||||
public static IQueryable<WorkOrder> ApplyTypeFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<WorkOrderType>? types)
|
||||
=> ApplyTypeAndOverdueFilter(query, types);
|
||||
IReadOnlyList<WorkOrderType>? types,
|
||||
bool overdue = false)
|
||||
=> ApplyTypeAndOverdueFilter(query, types, overdue);
|
||||
|
||||
public static IQueryable<WorkOrder> ApplySiteFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace SeaHaven.DataServices.Implementation
|
|||
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyDateRangeFilter(baseQuery, query.DateFrom, query.DateTo);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplySiteFilter(baseQuery, query.Sites);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types, query.Overdue);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyDispatcherFilter(
|
||||
baseQuery, query.Dispatchers, query.MyWorkOrders, query.CurrentUserId);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyStatusFilter(baseQuery, query.Statuses);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ namespace SeaHaven.DataServices.Implementation
|
|||
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyDispatcherFilter(
|
||||
baseQuery, query.Dispatchers, query.MyWorkOrders, query.CurrentUserId);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types, query.Overdue);
|
||||
|
||||
var scheduledBase = baseQuery.Where(w =>
|
||||
(w.ScheduledDate != null
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
DateOnly DateTo,
|
||||
IReadOnlyList<string>? Sites,
|
||||
IReadOnlyList<WorkOrderType>? Types,
|
||||
bool Overdue,
|
||||
IReadOnlyList<string>? Dispatchers,
|
||||
IReadOnlyList<LifecycleStatus>? Statuses,
|
||||
IReadOnlyList<string>? PmTypes,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
IReadOnlyList<string>? Dispatchers,
|
||||
bool MyWorkOrders,
|
||||
IReadOnlyList<WorkOrderType>? Types,
|
||||
bool Overdue,
|
||||
string? Search,
|
||||
string? CurrentUserId);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ namespace SeaHaven.Services.DTOs
|
|||
public List<string>? Dispatchers { get; set; }
|
||||
public bool MyWorkOrders { get; set; }
|
||||
public List<WorkOrderType>? Types { get; set; }
|
||||
/// <summary>When true, include past-due (isPastDue) rows. Combined with Types via OR.</summary>
|
||||
public bool Overdue { get; set; }
|
||||
public string? Search { get; set; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ namespace SeaHaven.Services.DTOs
|
|||
public DateOnly? DateTo { get; set; }
|
||||
public List<string>? Sites { get; set; }
|
||||
public List<WorkOrderType>? Types { get; set; }
|
||||
/// <summary>When true, include past-due (isPastDue) rows. Combined with Types via OR.</summary>
|
||||
public bool Overdue { get; set; }
|
||||
public List<string>? Dispatchers { get; set; }
|
||||
public List<LifecycleStatus>? Statuses { get; set; }
|
||||
public List<string>? PmTypes { get; set; }
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ namespace SeaHaven.Services.Implementation
|
|||
dateRange.To,
|
||||
query.Sites,
|
||||
query.Types,
|
||||
query.Overdue,
|
||||
query.Dispatchers,
|
||||
query.Statuses,
|
||||
query.PmTypes,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ namespace SeaHaven.Services.Implementation
|
|||
query.Dispatchers,
|
||||
query.MyWorkOrders,
|
||||
query.Types,
|
||||
query.Overdue,
|
||||
query.Search,
|
||||
currentUserId);
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ public class WorkOrderBoardSearchCountsTests
|
|||
null,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
"BK5",
|
||||
null));
|
||||
|
||||
|
|
@ -288,7 +289,7 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchAsync_OverdueTypeSentinel_FiltersPastDueOnly()
|
||||
public async Task SearchAsync_TypesOther_ReturnsOtherRowsNotPastDueSentinel()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var today = DateTime.UtcNow.Date;
|
||||
|
|
@ -324,11 +325,114 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
Types = new List<WorkOrderType> { WorkOrderType.Other }
|
||||
}, null);
|
||||
|
||||
Assert.Equal(1, result.TotalCount);
|
||||
Assert.Equal(2, result.Items.First().Id);
|
||||
Assert.Equal(WorkOrderType.Other, result.Items.First().WorkOrderType);
|
||||
Assert.False(result.Items.First().IsPastDue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchAsync_OverdueTrue_ReturnsPastDueRowsOnly()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var today = DateTime.UtcNow.Date;
|
||||
|
||||
context.workOrders.AddRange(
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
ScheduledDate = today.AddDays(-3),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000001"
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 2,
|
||||
WorkOrderType = WorkOrderType.Other,
|
||||
ScheduledDate = today.AddDays(3),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000002"
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 3,
|
||||
WorkOrderType = WorkOrderType.Reactive,
|
||||
ScheduledDate = today.AddDays(1),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000003"
|
||||
});
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var dataService = new WorkOrderAdvancedSearchDataService(context);
|
||||
var service = new SeaHaven.Services.Implementation.WorkOrderAdvancedSearchService(dataService);
|
||||
|
||||
var result = await service.SearchAsync(new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto
|
||||
{
|
||||
DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.Custom,
|
||||
DateFrom = DateOnly.FromDateTime(today.AddMonths(-1)),
|
||||
DateTo = DateOnly.FromDateTime(today.AddMonths(1)),
|
||||
Overdue = true
|
||||
}, null);
|
||||
|
||||
Assert.Equal(1, result.TotalCount);
|
||||
Assert.Equal(1, result.Items.First().Id);
|
||||
Assert.True(result.Items.First().IsPastDue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchAsync_OtherPlusOverdue_ReturnsUnion()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var today = DateTime.UtcNow.Date;
|
||||
|
||||
context.workOrders.AddRange(
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
ScheduledDate = today.AddDays(-3),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000001"
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 2,
|
||||
WorkOrderType = WorkOrderType.Other,
|
||||
ScheduledDate = today.AddDays(3),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000002"
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 3,
|
||||
WorkOrderType = WorkOrderType.Reactive,
|
||||
ScheduledDate = today.AddDays(1),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000003"
|
||||
});
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var dataService = new WorkOrderAdvancedSearchDataService(context);
|
||||
var service = new SeaHaven.Services.Implementation.WorkOrderAdvancedSearchService(dataService);
|
||||
|
||||
var result = await service.SearchAsync(new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto
|
||||
{
|
||||
DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.Custom,
|
||||
DateFrom = DateOnly.FromDateTime(today.AddMonths(-1)),
|
||||
DateTo = DateOnly.FromDateTime(today.AddMonths(1)),
|
||||
Types = new List<WorkOrderType> { WorkOrderType.Other },
|
||||
Overdue = true
|
||||
}, null);
|
||||
|
||||
Assert.Equal(2, result.TotalCount);
|
||||
Assert.Contains(result.Items, i => i.Id == 1 && i.IsPastDue);
|
||||
Assert.Contains(result.Items, i => i.Id == 2 && i.WorkOrderType == WorkOrderType.Other);
|
||||
Assert.DoesNotContain(result.Items, i => i.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchAsync_IncludesUnscheduledRows()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ public class WorkOrderBoardDataServiceTests
|
|||
null,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null));
|
||||
|
||||
|
|
@ -109,6 +110,7 @@ public class WorkOrderBoardDataServiceTests
|
|||
new[] { "user-a" },
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null));
|
||||
|
||||
|
|
@ -147,6 +149,7 @@ public class WorkOrderBoardDataServiceTests
|
|||
null,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
"me"));
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ Advanced search cross-week paginado.
|
|||
| `datePreset` | enum | `thisWeek`, `lastWeek`, `thisMonth`, `last3Months`, `nextWeek`, `nextMonth`, `custom` |
|
||||
| `dateFrom` / `dateTo` | date | Obrigatórios se `datePreset=custom` |
|
||||
| `sites` | string[] | `SiteCode` |
|
||||
| `types` | WorkOrderType[] | Tipo WO |
|
||||
| `types` | WorkOrderType[] | Tipo WO (inclui `Other` como tipo real) |
|
||||
| `overdue` | bool | Past-due (`isPastDue`); default `false`; OR com `types` |
|
||||
| `dispatchers` | string[] | GUIDs + `__unassigned__` |
|
||||
| `statuses` | LifecycleStatus[] | Status operacional |
|
||||
| `pmTypes` | string[] | Match em `Trade`/`Problem` (contains, case-insensitive) |
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue