mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
feat(work-orders): enrich board search overdue filters and 0-based paging (#23)
* feat(work-orders): enrich board search overdue filters and 0-based paging * fix(work-orders): align stacked services with CI build * fix(tests): pass userDataService in comment service unit test * 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. * test(work-orders): cover overdue date/status boundary and Other type-filter Lock the PR #23 overdue regression boundary through the public advanced search service. Prove overdue filtering is driven by past-due date plus non-terminal status, not by the WorkOrderType.Other sentinel: - Other + future/not-completed excluded from overdue - past-due + Scheduled included; past-due + Completed/Canceled excluded - types=[PM, Other] keeps real Other rows and does not pull past-due rows - assert 0-based paging (Page=0) is preserved alongside overdue/type filters --------- Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com> Co-authored-by: Alexandre Brandizzi <alex_brandizzi@hotmail.com>
This commit is contained in:
parent
833fb816ee
commit
620a36af54
22 changed files with 675 additions and 34 deletions
|
|
@ -4,6 +4,7 @@ using FluentAssertions;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moq;
|
||||
using SeaHaven.DataServices.Implementation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Helpers;
|
||||
|
|
@ -60,4 +61,17 @@ public class WorkOrderControllerSearchTests
|
|||
body.Status.Should().Be("Error");
|
||||
body.Message.Should().Contain("dateFrom");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBoard_WhenWeekEndBeforeWeekStart_ReturnsBadRequest()
|
||||
{
|
||||
var controller = NewController(Mock.Of<IWorkOrderAdvancedSearchService>());
|
||||
|
||||
var result = await controller.GetBoard(
|
||||
new DateOnly(2026, 6, 22),
|
||||
new DateOnly(2026, 6, 15));
|
||||
|
||||
var badRequest = result.Should().BeOfType<BadRequestObjectResult>().Subject;
|
||||
badRequest.Value.Should().BeOfType<string>().Which.Should().Contain("weekEnd");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,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)
|
||||
{
|
||||
var resolvedWeekEnd = weekEnd ?? weekStart.AddDays(4);
|
||||
|
|
@ -70,6 +71,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
Dispatchers = dispatchers,
|
||||
MyWorkOrders = myWorkOrders,
|
||||
Types = types,
|
||||
Overdue = overdue,
|
||||
Search = search
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -222,8 +222,8 @@ namespace Data.SeaHavenIndustries
|
|||
public string? Initials { get; set; }
|
||||
public string? Color { get; set; }
|
||||
public int? Type { get; set; } // 1 for users 0 for admin
|
||||
public ICollection<Template>? Templates { get; set; }
|
||||
public ICollection<WorkOrder>? WorkOrders { get; set; }
|
||||
public ICollection<Template>? Templates { get; set; }
|
||||
public ICollection<WorkOrder>? WorkOrders { get; set; }
|
||||
}
|
||||
public class FullAuditEntity
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,16 +42,53 @@ namespace SeaHaven.DataServices.Helpers
|
|||
return query.Where(w => w.AssignTo != null && ids.Contains(w.AssignTo));
|
||||
}
|
||||
|
||||
public static IQueryable<WorkOrder> ApplyTypeFilter(
|
||||
/// <summary>
|
||||
/// 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)
|
||||
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;
|
||||
|
||||
return query.Where(w => w.WorkOrderType != null && types.Contains(w.WorkOrderType.Value));
|
||||
var today = (utcNow ?? DateTime.UtcNow).Date;
|
||||
var typeFilters = hasTypes ? types!.ToList() : new List<WorkOrderType>();
|
||||
|
||||
// Keep past-due predicate inline — EF cannot translate local method calls.
|
||||
if (overdue && !hasTypes)
|
||||
{
|
||||
return query.Where(w =>
|
||||
w.ScheduledDate != null
|
||||
&& w.ScheduledDate.Value.Date < today
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled);
|
||||
}
|
||||
|
||||
if (overdue)
|
||||
{
|
||||
return query.Where(w =>
|
||||
(w.ScheduledDate != null
|
||||
&& w.ScheduledDate.Value.Date < today
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled)
|
||||
|| (w.WorkOrderType != null && typeFilters.Contains(w.WorkOrderType.Value)));
|
||||
}
|
||||
|
||||
return query.Where(w => w.WorkOrderType != null && typeFilters.Contains(w.WorkOrderType.Value));
|
||||
}
|
||||
|
||||
public static IQueryable<WorkOrder> ApplyTypeFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<WorkOrderType>? types,
|
||||
bool overdue = false)
|
||||
=> ApplyTypeAndOverdueFilter(query, types, overdue);
|
||||
|
||||
#region Phase 2 filter helpers (not wired in Phase 1 board API)
|
||||
|
||||
public static IQueryable<WorkOrder> ApplySiteFilter(
|
||||
|
|
@ -139,7 +176,10 @@ namespace SeaHaven.DataServices.Helpers
|
|||
|| (w.ScheduleWeekOnly == true
|
||||
&& w.TargetWeek != null
|
||||
&& w.TargetWeek >= dateFrom
|
||||
&& w.TargetWeek <= dateTo));
|
||||
&& w.TargetWeek <= dateTo)
|
||||
|| (w.ScheduledDate == null
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
|
|
@ -5,6 +7,10 @@ namespace SeaHaven.DataServices.Helpers
|
|||
{
|
||||
public static class WorkOrderBoardSearchFilter
|
||||
{
|
||||
private static readonly Regex PartialDatePattern = new(
|
||||
@"^\d{1,2}/\d{1,2}$",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
public static string? NormalizeSearch(string? search)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(search))
|
||||
|
|
@ -21,10 +27,23 @@ namespace SeaHaven.DataServices.Helpers
|
|||
return query;
|
||||
|
||||
var s = normalized.ToLower();
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var exactDate = TryParseExactDate(normalized);
|
||||
var hasExactDate = exactDate.HasValue;
|
||||
var exactDateValue = exactDate?.Date ?? default;
|
||||
|
||||
var partialMonthDay = TryParsePartialMonthDay(normalized);
|
||||
var hasPartialMonthDay = partialMonthDay.HasValue;
|
||||
var partialMonth = partialMonthDay?.Month ?? 0;
|
||||
var partialDay = partialMonthDay?.Day ?? 0;
|
||||
|
||||
// Keep predicates inline — EF Core cannot translate local method calls.
|
||||
return query.Where(w =>
|
||||
(w.SiteCode ?? "").ToLower().Contains(s)
|
||||
|| (w.InternalWONumber ?? "").ToLower().Contains(s)
|
||||
|| (w.WorkerOrderNumber ?? "").ToLower().Contains(s)
|
||||
|| (w.WorkerOrderTitle ?? "").ToLower().Contains(s)
|
||||
|| (w.Description ?? "").ToLower().Contains(s)
|
||||
|| (w.Locations != null && (w.Locations.Name ?? "").ToLower().Contains(s))
|
||||
|| (w.AssignToUser != null && ((w.AssignToUser.FirstName ?? "") + " " + (w.AssignToUser.LastName ?? "")).ToLower().Contains(s))
|
||||
|| (w.Trade ?? "").ToLower().Contains(s)
|
||||
|
|
@ -32,6 +51,19 @@ namespace SeaHaven.DataServices.Helpers
|
|||
|| (w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null && (w.PrimaryDispatch.Vendor.CompanyName ?? "").ToLower().Contains(s))
|
||||
|| (w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null && (w.PrimaryDispatch.Vendor.ContactName ?? "").ToLower().Contains(s))
|
||||
|| (w.Status ?? "").ToLower().Contains(s)
|
||||
|| (w.WorkOrderType == WorkOrderType.PO && "po".Contains(s))
|
||||
|| (w.WorkOrderType == WorkOrderType.PM && "pm".Contains(s))
|
||||
|| (w.WorkOrderType == WorkOrderType.Emergency && "emergency".Contains(s))
|
||||
|| (w.WorkOrderType == WorkOrderType.Project && "project".Contains(s))
|
||||
|| (w.WorkOrderType == WorkOrderType.Inspection && "inspection".Contains(s))
|
||||
|| (w.WorkOrderType == WorkOrderType.Reactive && "reactive".Contains(s))
|
||||
|| (w.WorkOrderType == WorkOrderType.AddOn && ("add-on".Contains(s) || "addon".Contains(s)))
|
||||
|| (w.WorkOrderType == WorkOrderType.Other && "other".Contains(s))
|
||||
|| ("overdue".Contains(s)
|
||||
&& w.ScheduledDate != null
|
||||
&& w.ScheduledDate.Value.Date < today
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled)
|
||||
|| (w.LifecycleStatus == LifecycleStatus.Incomplete && "incomplete".Contains(s))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.Pending && ("pending".Contains(s) || "on hold".Contains(s) || "onhold".Contains(s)))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.Scheduled && "scheduled".Contains(s))
|
||||
|
|
@ -41,7 +73,61 @@ namespace SeaHaven.DataServices.Helpers
|
|||
|| (w.LifecycleStatus == LifecycleStatus.Completed && ("completed".Contains(s) || "complete".Contains(s) || "done".Contains(s) || "closed".Contains(s)))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.Rescheduled && "rescheduled".Contains(s))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.Canceled && ("canceled".Contains(s) || "cancelled".Contains(s)))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.PendingQuote && ("pending quote".Contains(s) || "pendingquote".Contains(s) || "pending approval".Contains(s) || "pendingapproval".Contains(s))));
|
||||
|| (w.LifecycleStatus == LifecycleStatus.PendingQuote && ("pending quote".Contains(s) || "pendingquote".Contains(s) || "pending approval".Contains(s) || "pendingapproval".Contains(s)))
|
||||
|| (hasExactDate
|
||||
&& ((w.ScheduledDate != null && w.ScheduledDate.Value.Date == exactDateValue)
|
||||
|| (w.DueDate != null && w.DueDate.Value.Date == exactDateValue)))
|
||||
|| (hasPartialMonthDay
|
||||
&& ((w.ScheduledDate != null
|
||||
&& w.ScheduledDate.Value.Month == partialMonth
|
||||
&& w.ScheduledDate.Value.Day == partialDay)
|
||||
|| (w.DueDate != null
|
||||
&& w.DueDate.Value.Month == partialMonth
|
||||
&& w.DueDate.Value.Day == partialDay))));
|
||||
}
|
||||
|
||||
internal static DateTime? TryParseExactDate(string search)
|
||||
{
|
||||
if (PartialDatePattern.IsMatch(search))
|
||||
return null;
|
||||
|
||||
var formats = new[]
|
||||
{
|
||||
"yyyy-MM-dd",
|
||||
"MM/dd/yyyy",
|
||||
"M/d/yyyy",
|
||||
"MM/dd/yy",
|
||||
"M/d/yy"
|
||||
};
|
||||
|
||||
if (DateTime.TryParseExact(search, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed))
|
||||
return parsed.Date;
|
||||
|
||||
if (DateTime.TryParse(search, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed))
|
||||
return parsed.Date;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static (int Month, int Day)? TryParsePartialMonthDay(string search)
|
||||
{
|
||||
if (!PartialDatePattern.IsMatch(search))
|
||||
return null;
|
||||
|
||||
var parts = search.Split('/');
|
||||
if (parts.Length != 2)
|
||||
return null;
|
||||
|
||||
if (!int.TryParse(parts[0], NumberStyles.None, CultureInfo.InvariantCulture, out var month))
|
||||
return null;
|
||||
|
||||
if (!int.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var day))
|
||||
return null;
|
||||
|
||||
if (month is < 1 or > 12 || day is < 1 or > 31)
|
||||
return null;
|
||||
|
||||
return (month, day);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -35,7 +35,7 @@ namespace SeaHaven.DataServices.Implementation
|
|||
|
||||
var sorted = ApplySort(baseQuery, query.SortBy, query.SortDir);
|
||||
var paged = sorted
|
||||
.Skip((query.Page - 1) * query.PageSize)
|
||||
.Skip(query.Page * query.PageSize)
|
||||
.Take(query.PageSize);
|
||||
|
||||
var rows = await WorkOrderBoardProjection.ProjectRowsAsync(paged, isUnscheduled: false);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ public class PagedResultTests
|
|||
{
|
||||
[Theory]
|
||||
[InlineData(0, 10, 0)]
|
||||
[InlineData(1, 10, 1)]
|
||||
[InlineData(10, 10, 1)]
|
||||
[InlineData(25, 10, 3)]
|
||||
[InlineData(11, 10, 2)]
|
||||
[InlineData(100, 25, 4)]
|
||||
public void TotalPages_IsCeilingOfCountOverPageSize(int totalCount, int pageSize, int expectedPages)
|
||||
{
|
||||
|
|
@ -19,16 +20,32 @@ public class PagedResultTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void HasPrevious_IsFalseOnFirstPage_TrueAfterward()
|
||||
public void HasPrevious_IsFalseOnFirstPage_TrueAfterward_OneBased()
|
||||
{
|
||||
new PagedResult<string> { Page = 1, PageSize = 10, TotalCount = 30 }.HasPrevious.Should().BeFalse();
|
||||
new PagedResult<string> { Page = 2, PageSize = 10, TotalCount = 30 }.HasPrevious.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasNext_IsTrueBeforeLastPage_FalseOnLastPage()
|
||||
public void HasNext_IsTrueBeforeLastPage_FalseOnLastPage_OneBased()
|
||||
{
|
||||
new PagedResult<string> { Page = 2, PageSize = 10, TotalCount = 30 }.HasNext.Should().BeTrue();
|
||||
new PagedResult<string> { Page = 3, PageSize = 10, TotalCount = 30 }.HasNext.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasPrevious_And_HasNext_ZeroBased_WhenPageBaseIsZero()
|
||||
{
|
||||
var first = new PagedResult<string> { Page = 0, PageSize = 10, TotalCount = 30, PageBase = 0 };
|
||||
first.HasPrevious.Should().BeFalse();
|
||||
first.HasNext.Should().BeTrue();
|
||||
|
||||
var middle = new PagedResult<string> { Page = 1, PageSize = 10, TotalCount = 30, PageBase = 0 };
|
||||
middle.HasPrevious.Should().BeTrue();
|
||||
middle.HasNext.Should().BeTrue();
|
||||
|
||||
var last = new PagedResult<string> { Page = 2, PageSize = 10, TotalCount = 30, PageBase = 0 };
|
||||
last.HasPrevious.Should().BeTrue();
|
||||
last.HasNext.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,8 +48,17 @@ namespace SeaHaven.Services.DTOs
|
|||
public int TotalCount { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
|
||||
public bool HasPrevious => Page > 1;
|
||||
public bool HasNext => Page < TotalPages;
|
||||
|
||||
/// <summary>
|
||||
/// Page numbering base for <see cref="HasPrevious"/> / <see cref="HasNext"/>.
|
||||
/// Default <c>1</c> (classic APIs). Work-order advanced search sets <c>0</c>.
|
||||
/// </summary>
|
||||
public int PageBase { get; set; } = 1;
|
||||
|
||||
public int TotalPages => PageSize <= 0 ? 0 : (int)Math.Ceiling(TotalCount / (double)PageSize);
|
||||
|
||||
public bool HasPrevious => PageBase <= 0 ? Page > 0 : Page > 1;
|
||||
|
||||
public bool HasNext => PageBase <= 0 ? Page + 1 < TotalPages : Page < TotalPages;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ namespace SeaHaven.Services.DTOs
|
|||
/// <summary>Advanced search / filter query for the board.</summary>
|
||||
public class WorkOrderAdvancedSearchQueryDto
|
||||
{
|
||||
public int Page { get; set; } = 1;
|
||||
public int Page { get; set; } = 0;
|
||||
public int PageSize { get; set; } = 50;
|
||||
public string? Search { get; set; }
|
||||
public WorkOrderAdvancedSearchDatePreset? DatePreset { 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; }
|
||||
|
|
|
|||
29
SeaHaven.Services/Helpers/WorkOrderBoardSortFields.cs
Normal file
29
SeaHaven.Services/Helpers/WorkOrderBoardSortFields.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderBoardSortFields
|
||||
{
|
||||
public static readonly IReadOnlySet<string> Allowed = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"scheduledDate",
|
||||
"woNumber",
|
||||
"dueDate"
|
||||
};
|
||||
|
||||
public static string NormalizeOrThrow(string? sortBy)
|
||||
{
|
||||
var field = (sortBy ?? "scheduledDate").Trim();
|
||||
if (!Allowed.Contains(field))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"sortBy must be one of: scheduledDate, woNumber, dueDate.");
|
||||
}
|
||||
|
||||
return field.ToLowerInvariant() switch
|
||||
{
|
||||
"wonumber" => "woNumber",
|
||||
"duedate" => "dueDate",
|
||||
_ => "scheduledDate"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -56,19 +56,19 @@ namespace SeaHaven.Services.Helpers
|
|||
private static WorkOrderSearchDateRange ThisIsoWeek(DateOnly today)
|
||||
{
|
||||
var start = GetIsoWeekStart(today);
|
||||
return new WorkOrderSearchDateRange(start, start.AddDays(6));
|
||||
return new WorkOrderSearchDateRange(start, start.AddDays(4));
|
||||
}
|
||||
|
||||
private static WorkOrderSearchDateRange LastIsoWeek(DateOnly today)
|
||||
{
|
||||
var start = GetIsoWeekStart(today).AddDays(-7);
|
||||
return new WorkOrderSearchDateRange(start, start.AddDays(6));
|
||||
return new WorkOrderSearchDateRange(start, start.AddDays(4));
|
||||
}
|
||||
|
||||
private static WorkOrderSearchDateRange NextIsoWeek(DateOnly today)
|
||||
{
|
||||
var start = GetIsoWeekStart(today).AddDays(7);
|
||||
return new WorkOrderSearchDateRange(start, start.AddDays(6));
|
||||
return new WorkOrderSearchDateRange(start, start.AddDays(4));
|
||||
}
|
||||
|
||||
private static WorkOrderSearchDateRange ThisMonth(DateOnly today)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace SeaHaven.Services.Implementation
|
|||
{
|
||||
public class WorkOrderAdvancedSearchService : IWorkOrderAdvancedSearchService
|
||||
{
|
||||
private const int DefaultPageSize = 50;
|
||||
private const int DefaultPageSize = 100;
|
||||
private const int MaxPageSize = 100;
|
||||
|
||||
private readonly IWorkOrderAdvancedSearchDataService _searchDataService;
|
||||
|
|
@ -22,7 +22,7 @@ namespace SeaHaven.Services.Implementation
|
|||
WorkOrderAdvancedSearchQueryDto query,
|
||||
string? currentUserId)
|
||||
{
|
||||
var page = query.Page < 1 ? 1 : query.Page;
|
||||
var page = query.Page < 0 ? 0 : query.Page;
|
||||
var pageSize = query.PageSize < 1 ? DefaultPageSize : Math.Min(query.PageSize, MaxPageSize);
|
||||
|
||||
var dateRange = WorkOrderSearchDateRangeResolver.Resolve(
|
||||
|
|
@ -30,12 +30,15 @@ namespace SeaHaven.Services.Implementation
|
|||
query.DateFrom,
|
||||
query.DateTo);
|
||||
|
||||
var sortBy = WorkOrderBoardSortFields.NormalizeOrThrow(query.SortBy);
|
||||
|
||||
var dataQuery = new WorkOrderAdvancedSearchQuery(
|
||||
WorkOrderBoardSearchFilter.NormalizeSearch(query.Search),
|
||||
dateRange.From,
|
||||
dateRange.To,
|
||||
query.Sites,
|
||||
query.Types,
|
||||
query.Overdue,
|
||||
query.Dispatchers,
|
||||
query.Statuses,
|
||||
query.PmTypes,
|
||||
|
|
@ -45,7 +48,7 @@ namespace SeaHaven.Services.Implementation
|
|||
currentUserId,
|
||||
page,
|
||||
pageSize,
|
||||
query.SortBy ?? "scheduledDate",
|
||||
sortBy,
|
||||
query.SortDir ?? "asc");
|
||||
|
||||
var result = await _searchDataService.SearchAsync(dataQuery);
|
||||
|
|
@ -56,7 +59,8 @@ namespace SeaHaven.Services.Implementation
|
|||
Items = result.Rows.Select(r => WorkOrderBoardService.MapRawRow(r, utcNow)).ToList(),
|
||||
TotalCount = result.TotalCount,
|
||||
Page = page,
|
||||
PageSize = pageSize
|
||||
PageSize = pageSize,
|
||||
PageBase = 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ namespace SeaHaven.Services.Implementation
|
|||
query.Dispatchers,
|
||||
query.MyWorkOrders,
|
||||
query.Types,
|
||||
query.Overdue,
|
||||
query.Search,
|
||||
currentUserId);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Exceptions;
|
||||
|
|
@ -11,13 +11,16 @@ namespace SeaHaven.Services.Implementation
|
|||
{
|
||||
private readonly IWorkOrderDetailDataService _detailData;
|
||||
private readonly ICommentDataService _commentData;
|
||||
private readonly IUserDataService _userDataService;
|
||||
|
||||
public WorkOrderCommentService(
|
||||
IWorkOrderDetailDataService detailData,
|
||||
ICommentDataService commentData)
|
||||
ICommentDataService commentData,
|
||||
IUserDataService userDataService)
|
||||
{
|
||||
_detailData = detailData;
|
||||
_commentData = commentData;
|
||||
_userDataService = userDataService;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WorkOrderCommentDto>?> GetCommentsAsync(int workOrderId)
|
||||
|
|
@ -26,7 +29,8 @@ namespace SeaHaven.Services.Implementation
|
|||
return null;
|
||||
|
||||
var comments = await _detailData.GetCommentsAsync(workOrderId);
|
||||
return WorkOrderCommentProjection.ToDtos(comments);
|
||||
var authorNames = await ResolveAuthorNamesAsync(comments);
|
||||
return WorkOrderCommentProjection.ToDtos(comments, authorNames);
|
||||
}
|
||||
|
||||
public async Task<WorkOrderCommentDto> AddCommentAsync(
|
||||
|
|
@ -53,7 +57,58 @@ namespace SeaHaven.Services.Implementation
|
|||
};
|
||||
|
||||
var saved = await _commentData.AddAsync(comment);
|
||||
return WorkOrderCommentProjection.ToDto(saved);
|
||||
var authorNames = await ResolveAuthorNamesAsync(new[] { saved });
|
||||
return WorkOrderCommentProjection.ToDto(saved, authorNames);
|
||||
}
|
||||
|
||||
public async Task<WorkOrderCommentDto> UpdateCommentAsync(
|
||||
int workOrderId,
|
||||
int commentId,
|
||||
WorkOrderCommentCreateDto request,
|
||||
string? actorId,
|
||||
bool isAdmin)
|
||||
{
|
||||
var workOrder = await _detailData.GetWorkOrderForMediaAsync(workOrderId);
|
||||
if (workOrder == null)
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Work order not found.");
|
||||
|
||||
if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus))
|
||||
throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Text))
|
||||
throw new WorkOrderBoardValidationException("InvalidValue", "Comment text is required.");
|
||||
|
||||
var comment = await _commentData.GetByIdAsync(commentId);
|
||||
if (comment == null || comment.WorkerOrderId != workOrderId || comment.IsDeleted == true)
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Comment not found.");
|
||||
|
||||
if (!IsBoardEditableComment(comment))
|
||||
throw new WorkOrderBoardValidationException(
|
||||
"NotEditable",
|
||||
"Only board comments (CommentType General) can be edited via this endpoint.");
|
||||
|
||||
var isAuthor = !string.IsNullOrWhiteSpace(actorId)
|
||||
&& string.Equals(comment.UserId, actorId, StringComparison.Ordinal);
|
||||
if (!isAuthor && !isAdmin)
|
||||
throw new WorkOrderBoardValidationException("Forbidden", "You are not allowed to edit this comment.");
|
||||
|
||||
comment.Commenttext = request.Text.Trim();
|
||||
comment.LastModificationTime = DateTime.UtcNow;
|
||||
await _commentData.UpdateAsync(comment);
|
||||
|
||||
var authorNames = await ResolveAuthorNamesAsync(new[] { comment });
|
||||
return WorkOrderCommentProjection.ToDto(comment, authorNames);
|
||||
}
|
||||
|
||||
private static bool IsBoardEditableComment(Comments comment)
|
||||
=> string.Equals(comment.CommentType, "General", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(comment.RecordType, "WorkOrder", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private async Task<IReadOnlyDictionary<string, string>> ResolveAuthorNamesAsync(
|
||||
IEnumerable<Comments> comments)
|
||||
{
|
||||
var authorIds = WorkOrderCommentProjection.CollectAuthorUserIds(comments);
|
||||
return await _userDataService.GetDisplayNamesByIdsAsync(authorIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Interfaces
|
||||
|
|
@ -7,5 +7,11 @@ namespace SeaHaven.Services.Interfaces
|
|||
{
|
||||
Task<IReadOnlyList<WorkOrderCommentDto>?> GetCommentsAsync(int workOrderId);
|
||||
Task<WorkOrderCommentDto> AddCommentAsync(int workOrderId, WorkOrderCommentCreateDto request, string? actorId, string? documentUrl = null);
|
||||
Task<WorkOrderCommentDto> UpdateCommentAsync(
|
||||
int workOrderId,
|
||||
int commentId,
|
||||
WorkOrderCommentCreateDto request,
|
||||
string? actorId,
|
||||
bool isAdmin);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ public class WorkOrderBoardSearchFilterTests
|
|||
[InlineData("HVAC Tech", 1)]
|
||||
[InlineData("Acme HVAC", 1)]
|
||||
[InlineData("sched", 1)]
|
||||
[InlineData("Leak repair", 1)]
|
||||
[InlineData("Reactive", 1)]
|
||||
[InlineData("6/23", 1)]
|
||||
public async Task Apply_MatchesExpectedField(string search, int expectedId)
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
|
|
@ -73,6 +76,9 @@ public class WorkOrderBoardSearchFilterTests
|
|||
Id = 1,
|
||||
SiteCode = "BK5",
|
||||
InternalWONumber = "10000000001",
|
||||
WorkerOrderTitle = "Leak repair",
|
||||
WorkOrderType = WorkOrderType.Reactive,
|
||||
ScheduledDate = new DateTime(2026, 6, 23),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
PrimaryDispatchId = 10
|
||||
});
|
||||
|
|
@ -128,6 +134,7 @@ public class WorkOrderBoardSearchCountsTests
|
|||
null,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
"BK5",
|
||||
null));
|
||||
|
||||
|
|
@ -152,7 +159,7 @@ public class WorkOrderSearchDateRangeResolverTests
|
|||
reference);
|
||||
|
||||
Assert.Equal(new DateOnly(2026, 6, 22), range.From);
|
||||
Assert.Equal(new DateOnly(2026, 6, 28), range.To);
|
||||
Assert.Equal(new DateOnly(2026, 6, 26), range.To);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -213,7 +220,7 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
DateFrom = weekStart,
|
||||
DateTo = weekStart.AddDays(6),
|
||||
Sites = new List<string> { "BK5" },
|
||||
Page = 1,
|
||||
Page = 0,
|
||||
PageSize = 50
|
||||
}, null);
|
||||
|
||||
|
|
@ -237,4 +244,365 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
|
||||
Assert.Equal(100, result.PageSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchAsync_PageZero_ReturnsFirstPage()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var weekStart = new DateOnly(2026, 6, 22);
|
||||
|
||||
context.workOrders.AddRange(
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
SiteCode = "BK5",
|
||||
ScheduledDate = new DateTime(2026, 6, 23),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000001"
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 2,
|
||||
SiteCode = "BK6",
|
||||
ScheduledDate = new DateTime(2026, 6, 24),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000002"
|
||||
});
|
||||
|
||||
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 = weekStart,
|
||||
DateTo = weekStart.AddDays(4),
|
||||
Page = 0,
|
||||
PageSize = 1
|
||||
}, null);
|
||||
|
||||
Assert.Equal(2, result.TotalCount);
|
||||
Assert.Single(result.Items);
|
||||
Assert.Equal(0, result.Page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchAsync_TypesOther_ReturnsOtherRowsNotPastDueSentinel()
|
||||
{
|
||||
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"
|
||||
});
|
||||
|
||||
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 }
|
||||
}, 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_OverdueDerivesFromDateAndStatus_NotWorkOrderType()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var today = DateTime.UtcNow.Date;
|
||||
|
||||
context.workOrders.AddRange(
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
WorkOrderType = WorkOrderType.Other,
|
||||
ScheduledDate = today.AddDays(5),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000001"
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 2,
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
ScheduledDate = today.AddDays(-3),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000002"
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 3,
|
||||
WorkOrderType = WorkOrderType.Reactive,
|
||||
ScheduledDate = today.AddDays(-2),
|
||||
LifecycleStatus = LifecycleStatus.Completed,
|
||||
InternalWONumber = "10000000003"
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 4,
|
||||
WorkOrderType = WorkOrderType.Reactive,
|
||||
ScheduledDate = today.AddDays(-1),
|
||||
LifecycleStatus = LifecycleStatus.Canceled,
|
||||
InternalWONumber = "10000000004"
|
||||
});
|
||||
|
||||
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,
|
||||
Page = 0,
|
||||
PageSize = 50
|
||||
}, null);
|
||||
|
||||
var overdue = Assert.Single(result.Items);
|
||||
Assert.Equal(0, result.Page);
|
||||
Assert.Equal(2, overdue.Id);
|
||||
Assert.True(overdue.IsPastDue);
|
||||
Assert.DoesNotContain(result.Items, i => i.Id == 1);
|
||||
Assert.DoesNotContain(result.Items, i => i.Id == 3);
|
||||
Assert.DoesNotContain(result.Items, i => i.Id == 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchAsync_TreatsOtherAsRealTypeFilter_NotOverdueSentinel()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var today = DateTime.UtcNow.Date;
|
||||
|
||||
context.workOrders.AddRange(
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
ScheduledDate = today.AddDays(2),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000001"
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 2,
|
||||
WorkOrderType = WorkOrderType.Other,
|
||||
ScheduledDate = today.AddDays(2),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000002"
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 3,
|
||||
WorkOrderType = WorkOrderType.Reactive,
|
||||
ScheduledDate = today.AddDays(-3),
|
||||
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.PM, WorkOrderType.Other },
|
||||
Page = 0,
|
||||
PageSize = 50
|
||||
}, null);
|
||||
|
||||
Assert.Equal(2, result.TotalCount);
|
||||
Assert.Equal(0, result.Page);
|
||||
Assert.Contains(result.Items, i => i.Id == 1 && i.WorkOrderType == WorkOrderType.PM);
|
||||
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()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var weekStart = new DateOnly(2026, 6, 22);
|
||||
|
||||
context.workOrders.AddRange(
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
ScheduledDate = new DateTime(2026, 6, 23),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000001"
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 2,
|
||||
ScheduledDate = null,
|
||||
LifecycleStatus = LifecycleStatus.Incomplete,
|
||||
InternalWONumber = "10000000002"
|
||||
});
|
||||
|
||||
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 = weekStart,
|
||||
DateTo = weekStart.AddDays(4)
|
||||
}, null);
|
||||
|
||||
Assert.Equal(2, result.TotalCount);
|
||||
Assert.Contains(result.Items, i => i.Id == 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchAsync_InvalidSortBy_Throws()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var dataService = new WorkOrderAdvancedSearchDataService(context);
|
||||
var service = new SeaHaven.Services.Implementation.WorkOrderAdvancedSearchService(dataService);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<ArgumentException>(() => service.SearchAsync(
|
||||
new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto
|
||||
{
|
||||
DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.ThisWeek,
|
||||
SortBy = "invalid"
|
||||
}, null));
|
||||
|
||||
Assert.Contains("sortBy", ex.Message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
||||
|
|
|
|||
|
|
@ -313,7 +313,8 @@ public class WorkOrderCommentServiceTests
|
|||
|
||||
var detailData = new WorkOrderDetailDataService(context);
|
||||
var commentData = new CommentDataService(context);
|
||||
var service = new WorkOrderCommentService(detailData, commentData);
|
||||
var userData = new UserDataService(context);
|
||||
var service = new WorkOrderCommentService(detailData, commentData, userData);
|
||||
|
||||
var result = await service.AddCommentAsync(1, new WorkOrderCommentCreateDto { Text = "Note" }, "user-abc");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue