From f6c7c1ec7f4fefaeb1938250b7e3e27e694ddc34 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Tue, 21 Jul 2026 10:36:21 -0300 Subject: [PATCH] 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. --- .../Controllers/WorkOrderController.cs | 2 + .../Helpers/WorkOrderBoardQueryFilters.cs | 20 ++-- .../WorkOrderAdvancedSearchDataService.cs | 2 +- .../WorkOrderBoardDataService.cs | 2 +- .../WorkOrderAdvancedSearchModels.cs | 1 + .../Interfaces/WorkOrderBoardModels.cs | 1 + SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs | 2 + .../DTOs/WorkOrderBoardRequestDTOs.cs | 2 + .../WorkOrderAdvancedSearchService.cs | 1 + .../Implementation/WorkOrderBoardService.cs | 1 + .../WorkOrderBoardSearchTests.cs | 106 +++++++++++++++++- .../WorkOrderBoardServiceTests.cs | 3 + docs/work-orders/phase-4/README.md | 3 +- 13 files changed, 134 insertions(+), 12 deletions(-) diff --git a/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs b/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs index cedc42f..db2b7fe 100644 --- a/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs +++ b/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs @@ -112,6 +112,7 @@ namespace Api.SeaHavenIndustries.Controllers [FromQuery] List? dispatchers = null, [FromQuery] bool myWorkOrders = false, [FromQuery] List? 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 }; diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs index 46069db..4384692 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs @@ -38,22 +38,25 @@ namespace SeaHaven.DataServices.Helpers } /// - /// FE sends (99) as the overdue/isPastDue sentinel — not a real type filter. + /// Filters by real (including ) + /// and/or past-due rows when is true. + /// Types and overdue are combined with OR when both are present. /// public static IQueryable ApplyTypeAndOverdueFilter( IQueryable query, IReadOnlyList? 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(); // 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 ApplyTypeFilter( IQueryable query, - IReadOnlyList? types) - => ApplyTypeAndOverdueFilter(query, types); + IReadOnlyList? types, + bool overdue = false) + => ApplyTypeAndOverdueFilter(query, types, overdue); public static IQueryable ApplySiteFilter( IQueryable query, diff --git a/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs b/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs index d30cb49..ab995c7 100644 --- a/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs +++ b/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs @@ -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); diff --git a/SeaHaven.DataServices/Implementation/WorkOrderBoardDataService.cs b/SeaHaven.DataServices/Implementation/WorkOrderBoardDataService.cs index 51936eb..ec38950 100644 --- a/SeaHaven.DataServices/Implementation/WorkOrderBoardDataService.cs +++ b/SeaHaven.DataServices/Implementation/WorkOrderBoardDataService.cs @@ -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 diff --git a/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs b/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs index 0cede89..5dc30c7 100644 --- a/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs +++ b/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs @@ -9,6 +9,7 @@ namespace SeaHaven.DataServices.Interfaces DateOnly DateTo, IReadOnlyList? Sites, IReadOnlyList? Types, + bool Overdue, IReadOnlyList? Dispatchers, IReadOnlyList? Statuses, IReadOnlyList? PmTypes, diff --git a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs index c46ff55..51ca0d7 100644 --- a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs +++ b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs @@ -8,6 +8,7 @@ namespace SeaHaven.DataServices.Interfaces IReadOnlyList? Dispatchers, bool MyWorkOrders, IReadOnlyList? Types, + bool Overdue, string? Search, string? CurrentUserId); diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs index f0b13f9..adba573 100644 --- a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs @@ -10,6 +10,8 @@ namespace SeaHaven.Services.DTOs public List? Dispatchers { get; set; } public bool MyWorkOrders { get; set; } public List? Types { get; set; } + /// When true, include past-due (isPastDue) rows. Combined with Types via OR. + public bool Overdue { get; set; } public string? Search { get; set; } } diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs index b78be76..1c8bf87 100644 --- a/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs @@ -51,6 +51,8 @@ namespace SeaHaven.Services.DTOs public DateOnly? DateTo { get; set; } public List? Sites { get; set; } public List? Types { get; set; } + /// When true, include past-due (isPastDue) rows. Combined with Types via OR. + public bool Overdue { get; set; } public List? Dispatchers { get; set; } public List? Statuses { get; set; } public List? PmTypes { get; set; } diff --git a/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs b/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs index 8b35a1a..cb5dd22 100644 --- a/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs @@ -38,6 +38,7 @@ namespace SeaHaven.Services.Implementation dateRange.To, query.Sites, query.Types, + query.Overdue, query.Dispatchers, query.Statuses, query.PmTypes, diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs index 56c576e..3bc6a8c 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs @@ -30,6 +30,7 @@ namespace SeaHaven.Services.Implementation query.Dispatchers, query.MyWorkOrders, query.Types, + query.Overdue, query.Search, currentUserId); diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs index 7580562..f2d37a9 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs @@ -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.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.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() { diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs index 8b1b867..2652ccd 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs @@ -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")); diff --git a/docs/work-orders/phase-4/README.md b/docs/work-orders/phase-4/README.md index d9b602f..602ee32 100644 --- a/docs/work-orders/phase-4/README.md +++ b/docs/work-orders/phase-4/README.md @@ -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) |