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] List<string>? dispatchers = null,
|
||||||
[FromQuery] bool myWorkOrders = false,
|
[FromQuery] bool myWorkOrders = false,
|
||||||
[FromQuery] List<WorkOrderType>? types = null,
|
[FromQuery] List<WorkOrderType>? types = null,
|
||||||
|
[FromQuery] bool overdue = false,
|
||||||
[FromQuery] string? search = null)
|
[FromQuery] string? search = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -124,6 +125,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
Dispatchers = dispatchers,
|
Dispatchers = dispatchers,
|
||||||
MyWorkOrders = myWorkOrders,
|
MyWorkOrders = myWorkOrders,
|
||||||
Types = types,
|
Types = types,
|
||||||
|
Overdue = overdue,
|
||||||
Search = search
|
Search = search
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,22 +38,25 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public static IQueryable<WorkOrder> ApplyTypeAndOverdueFilter(
|
public static IQueryable<WorkOrder> ApplyTypeAndOverdueFilter(
|
||||||
IQueryable<WorkOrder> query,
|
IQueryable<WorkOrder> query,
|
||||||
IReadOnlyList<WorkOrderType>? types,
|
IReadOnlyList<WorkOrderType>? types,
|
||||||
|
bool overdue = false,
|
||||||
DateTime? utcNow = null)
|
DateTime? utcNow = null)
|
||||||
{
|
{
|
||||||
if (types == null || types.Count == 0)
|
var hasTypes = types != null && types.Count > 0;
|
||||||
|
if (!hasTypes && !overdue)
|
||||||
return query;
|
return query;
|
||||||
|
|
||||||
var today = (utcNow ?? DateTime.UtcNow).Date;
|
var today = (utcNow ?? DateTime.UtcNow).Date;
|
||||||
var includeOverdue = types.Contains(WorkOrderType.Other);
|
var typeFilters = hasTypes ? types!.ToList() : new List<WorkOrderType>();
|
||||||
var typeFilters = types.Where(t => t != WorkOrderType.Other).ToList();
|
|
||||||
|
|
||||||
// Keep past-due predicate inline — EF cannot translate local method calls.
|
// Keep past-due predicate inline — EF cannot translate local method calls.
|
||||||
if (includeOverdue && typeFilters.Count == 0)
|
if (overdue && !hasTypes)
|
||||||
{
|
{
|
||||||
return query.Where(w =>
|
return query.Where(w =>
|
||||||
w.ScheduledDate != null
|
w.ScheduledDate != null
|
||||||
|
|
@ -62,7 +65,7 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
&& w.LifecycleStatus != LifecycleStatus.Canceled);
|
&& w.LifecycleStatus != LifecycleStatus.Canceled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeOverdue)
|
if (overdue)
|
||||||
{
|
{
|
||||||
return query.Where(w =>
|
return query.Where(w =>
|
||||||
(w.ScheduledDate != null
|
(w.ScheduledDate != null
|
||||||
|
|
@ -77,8 +80,9 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
|
|
||||||
public static IQueryable<WorkOrder> ApplyTypeFilter(
|
public static IQueryable<WorkOrder> ApplyTypeFilter(
|
||||||
IQueryable<WorkOrder> query,
|
IQueryable<WorkOrder> query,
|
||||||
IReadOnlyList<WorkOrderType>? types)
|
IReadOnlyList<WorkOrderType>? types,
|
||||||
=> ApplyTypeAndOverdueFilter(query, types);
|
bool overdue = false)
|
||||||
|
=> ApplyTypeAndOverdueFilter(query, types, overdue);
|
||||||
|
|
||||||
public static IQueryable<WorkOrder> ApplySiteFilter(
|
public static IQueryable<WorkOrder> ApplySiteFilter(
|
||||||
IQueryable<WorkOrder> query,
|
IQueryable<WorkOrder> query,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
||||||
baseQuery = WorkOrderBoardQueryFilters.ApplyDateRangeFilter(baseQuery, query.DateFrom, query.DateTo);
|
baseQuery = WorkOrderBoardQueryFilters.ApplyDateRangeFilter(baseQuery, query.DateFrom, query.DateTo);
|
||||||
baseQuery = WorkOrderBoardQueryFilters.ApplySiteFilter(baseQuery, query.Sites);
|
baseQuery = WorkOrderBoardQueryFilters.ApplySiteFilter(baseQuery, query.Sites);
|
||||||
baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types);
|
baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types, query.Overdue);
|
||||||
baseQuery = WorkOrderBoardQueryFilters.ApplyDispatcherFilter(
|
baseQuery = WorkOrderBoardQueryFilters.ApplyDispatcherFilter(
|
||||||
baseQuery, query.Dispatchers, query.MyWorkOrders, query.CurrentUserId);
|
baseQuery, query.Dispatchers, query.MyWorkOrders, query.CurrentUserId);
|
||||||
baseQuery = WorkOrderBoardQueryFilters.ApplyStatusFilter(baseQuery, query.Statuses);
|
baseQuery = WorkOrderBoardQueryFilters.ApplyStatusFilter(baseQuery, query.Statuses);
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
||||||
baseQuery = WorkOrderBoardQueryFilters.ApplyDispatcherFilter(
|
baseQuery = WorkOrderBoardQueryFilters.ApplyDispatcherFilter(
|
||||||
baseQuery, query.Dispatchers, query.MyWorkOrders, query.CurrentUserId);
|
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 =>
|
var scheduledBase = baseQuery.Where(w =>
|
||||||
(w.ScheduledDate != null
|
(w.ScheduledDate != null
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
DateOnly DateTo,
|
DateOnly DateTo,
|
||||||
IReadOnlyList<string>? Sites,
|
IReadOnlyList<string>? Sites,
|
||||||
IReadOnlyList<WorkOrderType>? Types,
|
IReadOnlyList<WorkOrderType>? Types,
|
||||||
|
bool Overdue,
|
||||||
IReadOnlyList<string>? Dispatchers,
|
IReadOnlyList<string>? Dispatchers,
|
||||||
IReadOnlyList<LifecycleStatus>? Statuses,
|
IReadOnlyList<LifecycleStatus>? Statuses,
|
||||||
IReadOnlyList<string>? PmTypes,
|
IReadOnlyList<string>? PmTypes,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
IReadOnlyList<string>? Dispatchers,
|
IReadOnlyList<string>? Dispatchers,
|
||||||
bool MyWorkOrders,
|
bool MyWorkOrders,
|
||||||
IReadOnlyList<WorkOrderType>? Types,
|
IReadOnlyList<WorkOrderType>? Types,
|
||||||
|
bool Overdue,
|
||||||
string? Search,
|
string? Search,
|
||||||
string? CurrentUserId);
|
string? CurrentUserId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ namespace SeaHaven.Services.DTOs
|
||||||
public List<string>? Dispatchers { get; set; }
|
public List<string>? Dispatchers { get; set; }
|
||||||
public bool MyWorkOrders { get; set; }
|
public bool MyWorkOrders { get; set; }
|
||||||
public List<WorkOrderType>? Types { 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; }
|
public string? Search { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@ namespace SeaHaven.Services.DTOs
|
||||||
public DateOnly? DateTo { get; set; }
|
public DateOnly? DateTo { get; set; }
|
||||||
public List<string>? Sites { get; set; }
|
public List<string>? Sites { get; set; }
|
||||||
public List<WorkOrderType>? Types { 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<string>? Dispatchers { get; set; }
|
||||||
public List<LifecycleStatus>? Statuses { get; set; }
|
public List<LifecycleStatus>? Statuses { get; set; }
|
||||||
public List<string>? PmTypes { get; set; }
|
public List<string>? PmTypes { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
dateRange.To,
|
dateRange.To,
|
||||||
query.Sites,
|
query.Sites,
|
||||||
query.Types,
|
query.Types,
|
||||||
|
query.Overdue,
|
||||||
query.Dispatchers,
|
query.Dispatchers,
|
||||||
query.Statuses,
|
query.Statuses,
|
||||||
query.PmTypes,
|
query.PmTypes,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
query.Dispatchers,
|
query.Dispatchers,
|
||||||
query.MyWorkOrders,
|
query.MyWorkOrders,
|
||||||
query.Types,
|
query.Types,
|
||||||
|
query.Overdue,
|
||||||
query.Search,
|
query.Search,
|
||||||
currentUserId);
|
currentUserId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ public class WorkOrderBoardSearchCountsTests
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
|
false,
|
||||||
"BK5",
|
"BK5",
|
||||||
null));
|
null));
|
||||||
|
|
||||||
|
|
@ -288,7 +289,7 @@ public class WorkOrderAdvancedSearchServiceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SearchAsync_OverdueTypeSentinel_FiltersPastDueOnly()
|
public async Task SearchAsync_TypesOther_ReturnsOtherRowsNotPastDueSentinel()
|
||||||
{
|
{
|
||||||
await using var context = CreateContext();
|
await using var context = CreateContext();
|
||||||
var today = DateTime.UtcNow.Date;
|
var today = DateTime.UtcNow.Date;
|
||||||
|
|
@ -324,11 +325,114 @@ public class WorkOrderAdvancedSearchServiceTests
|
||||||
Types = new List<WorkOrderType> { WorkOrderType.Other }
|
Types = new List<WorkOrderType> { WorkOrderType.Other }
|
||||||
}, null);
|
}, 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.TotalCount);
|
||||||
Assert.Equal(1, result.Items.First().Id);
|
Assert.Equal(1, result.Items.First().Id);
|
||||||
Assert.True(result.Items.First().IsPastDue);
|
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]
|
[Fact]
|
||||||
public async Task SearchAsync_IncludesUnscheduledRows()
|
public async Task SearchAsync_IncludesUnscheduledRows()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ public class WorkOrderBoardDataServiceTests
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
|
false,
|
||||||
null,
|
null,
|
||||||
null));
|
null));
|
||||||
|
|
||||||
|
|
@ -109,6 +110,7 @@ public class WorkOrderBoardDataServiceTests
|
||||||
new[] { "user-a" },
|
new[] { "user-a" },
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
|
false,
|
||||||
null,
|
null,
|
||||||
null));
|
null));
|
||||||
|
|
||||||
|
|
@ -147,6 +149,7 @@ public class WorkOrderBoardDataServiceTests
|
||||||
null,
|
null,
|
||||||
true,
|
true,
|
||||||
null,
|
null,
|
||||||
|
false,
|
||||||
null,
|
null,
|
||||||
"me"));
|
"me"));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ Advanced search cross-week paginado.
|
||||||
| `datePreset` | enum | `thisWeek`, `lastWeek`, `thisMonth`, `last3Months`, `nextWeek`, `nextMonth`, `custom` |
|
| `datePreset` | enum | `thisWeek`, `lastWeek`, `thisMonth`, `last3Months`, `nextWeek`, `nextMonth`, `custom` |
|
||||||
| `dateFrom` / `dateTo` | date | Obrigatórios se `datePreset=custom` |
|
| `dateFrom` / `dateTo` | date | Obrigatórios se `datePreset=custom` |
|
||||||
| `sites` | string[] | `SiteCode` |
|
| `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__` |
|
| `dispatchers` | string[] | GUIDs + `__unassigned__` |
|
||||||
| `statuses` | LifecycleStatus[] | Status operacional |
|
| `statuses` | LifecycleStatus[] | Status operacional |
|
||||||
| `pmTypes` | string[] | Match em `Trade`/`Problem` (contains, case-insensitive) |
|
| `pmTypes` | string[] | Match em `Trade`/`Problem` (contains, case-insensitive) |
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue