mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-05 07:12:10 +00:00
Merge branch 'dev' into fix/ab/sh-261-sh-265-vendor-directory
This commit is contained in:
commit
8406ffb20f
31 changed files with 1194 additions and 88 deletions
|
|
@ -54,7 +54,8 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
[FromQuery] bool myWorkOrders = false,
|
[FromQuery] bool myWorkOrders = false,
|
||||||
[FromQuery] List<WorkOrderType>? types = null,
|
[FromQuery] List<WorkOrderType>? types = null,
|
||||||
[FromQuery] bool overdue = false,
|
[FromQuery] bool overdue = false,
|
||||||
[FromQuery] string? search = null)
|
[FromQuery] string? search = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var resolvedWeekEnd = weekEnd ?? weekStart.AddDays(4);
|
var resolvedWeekEnd = weekEnd ?? weekStart.AddDays(4);
|
||||||
var weekValidationError = WorkOrderOperationalWeek.ValidateWeekWindow(weekStart, resolvedWeekEnd);
|
var weekValidationError = WorkOrderOperationalWeek.ValidateWeekWindow(weekStart, resolvedWeekEnd);
|
||||||
|
|
@ -75,7 +76,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
Search = search
|
Search = search
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await _workOrderBoardService.GetBoardAsync(query, User, userId);
|
var result = await _workOrderBoardService.GetBoardAsync(query, User, userId, cancellationToken);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,8 @@ namespace Data.SeaHavenIndustries.Enums
|
||||||
Before = 1,
|
Before = 1,
|
||||||
After = 2,
|
After = 2,
|
||||||
Extra = 3,
|
Extra = 3,
|
||||||
Completion = 4
|
Completion = 4,
|
||||||
|
Aveta = 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.SeaHavenIndustries.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260824150000_SH117_AvetaRequired")]
|
||||||
|
public partial class SH117_AvetaRequired : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "AvetaRequired",
|
||||||
|
table: "workOrders",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AvetaRequired",
|
||||||
|
table: "workOrders");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2410,6 +2410,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
||||||
b.Property<string>("Attachments")
|
b.Property<string>("Attachments")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("AvetaRequired")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<string>("AvettaTask")
|
b.Property<string>("AvettaTask")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,10 @@ namespace Data.SeaHavenIndustries
|
||||||
public string? ExtraServices { get; set; }
|
public string? ExtraServices { get; set; }
|
||||||
[MaxLength(4000)]
|
[MaxLength(4000)]
|
||||||
public string? ServiceNotes { get; set; }
|
public string? ServiceNotes { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Per-WO Aveta compliance flag. Distinct from legacy AvettaTask string.
|
||||||
|
/// </summary>
|
||||||
|
public bool AvetaRequired { get; set; }
|
||||||
/// <summary>Tech/crew phone when no primary dispatch; otherwise prefer Dispatch.TechPhone.</summary>
|
/// <summary>Tech/crew phone when no primary dispatch; otherwise prefer Dispatch.TechPhone.</summary>
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
public string? TechPhone { get; set; }
|
public string? TechPhone { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
|
||||||
|
|
@ -9,7 +10,8 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
public static async Task<IReadOnlyList<WorkOrderBoardRawRow>> ProjectRowsAsync(
|
public static async Task<IReadOnlyList<WorkOrderBoardRawRow>> ProjectRowsAsync(
|
||||||
ApplicationDbContext context,
|
ApplicationDbContext context,
|
||||||
IQueryable<WorkOrder> query,
|
IQueryable<WorkOrder> query,
|
||||||
bool isUnscheduled)
|
bool isUnscheduled,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var rows = await query
|
var rows = await query
|
||||||
.Select(w => new
|
.Select(w => new
|
||||||
|
|
@ -19,6 +21,12 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.RescheduleCount,
|
w.RescheduleCount,
|
||||||
w.CarriedOver,
|
w.CarriedOver,
|
||||||
w.IsAddOn,
|
w.IsAddOn,
|
||||||
|
w.AvetaRequired,
|
||||||
|
HasAvetaDocument = w.workOrderAttachments!.Any(a =>
|
||||||
|
(a.IsDeleted == null || a.IsDeleted == false)
|
||||||
|
&& a.Category == WorkOrderMediaCategory.Aveta),
|
||||||
|
w.OriginalDate,
|
||||||
|
w.OriginalWeek,
|
||||||
w.WorkOrderType,
|
w.WorkOrderType,
|
||||||
w.SiteCode,
|
w.SiteCode,
|
||||||
LocationName = w.Locations != null ? w.Locations.Name : null,
|
LocationName = w.Locations != null ? w.Locations.Name : null,
|
||||||
|
|
@ -74,48 +82,14 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.FlagColor,
|
w.FlagColor,
|
||||||
w.PrimaryDispatchId,
|
w.PrimaryDispatchId,
|
||||||
w.RowVersion,
|
w.RowVersion,
|
||||||
DispatchRowVersion = w.PrimaryDispatch != null ? w.PrimaryDispatch.RowVersion : null,
|
DispatchRowVersion = w.PrimaryDispatch != null ? w.PrimaryDispatch.RowVersion : null
|
||||||
// SH-188: aggregate Pending uplifts on every live dispatch tied to this WO
|
|
||||||
// (primary WorkOrderId and multi-WO DispatchWorkOrders links).
|
|
||||||
PendingUpliftCount = context.DispatchUpliftRequests.Count(u =>
|
|
||||||
u.Status == "Pending"
|
|
||||||
&& (u.IsDeleted == null || u.IsDeleted == false)
|
|
||||||
&& u.Dispatch != null
|
|
||||||
&& (u.Dispatch.IsDeleted == null || u.Dispatch.IsDeleted == false)
|
|
||||||
&& (
|
|
||||||
u.Dispatch.WorkOrderId == w.Id
|
|
||||||
|| u.Dispatch.DispatchWorkOrders!.Any(link => link.WorkOrderId == w.Id))),
|
|
||||||
HasUplift = context.DispatchUpliftRequests.Any(u =>
|
|
||||||
(u.IsDeleted == null || u.IsDeleted == false)
|
|
||||||
&& u.Dispatch != null
|
|
||||||
&& (u.Dispatch.IsDeleted == null || u.Dispatch.IsDeleted == false)
|
|
||||||
&& (
|
|
||||||
u.Dispatch.WorkOrderId == w.Id
|
|
||||||
|| u.Dispatch.DispatchWorkOrders!.Any(link => link.WorkOrderId == w.Id))),
|
|
||||||
PrimaryUpliftStatus = context.DispatchUpliftRequests
|
|
||||||
.Where(u =>
|
|
||||||
(u.IsDeleted == null || u.IsDeleted == false)
|
|
||||||
&& u.Dispatch != null
|
|
||||||
&& (u.Dispatch.IsDeleted == null || u.Dispatch.IsDeleted == false)
|
|
||||||
&& (
|
|
||||||
u.Dispatch.WorkOrderId == w.Id
|
|
||||||
|| u.Dispatch.DispatchWorkOrders!.Any(link => link.WorkOrderId == w.Id)))
|
|
||||||
.OrderByDescending(u => u.CreatedDate)
|
|
||||||
.Select(u => u.Status)
|
|
||||||
.FirstOrDefault(),
|
|
||||||
PrimaryUpliftAmount = context.DispatchUpliftRequests
|
|
||||||
.Where(u =>
|
|
||||||
(u.IsDeleted == null || u.IsDeleted == false)
|
|
||||||
&& u.Dispatch != null
|
|
||||||
&& (u.Dispatch.IsDeleted == null || u.Dispatch.IsDeleted == false)
|
|
||||||
&& (
|
|
||||||
u.Dispatch.WorkOrderId == w.Id
|
|
||||||
|| u.Dispatch.DispatchWorkOrders!.Any(link => link.WorkOrderId == w.Id)))
|
|
||||||
.OrderByDescending(u => u.CreatedDate)
|
|
||||||
.Select(u => (decimal?)u.RequestedNTE)
|
|
||||||
.FirstOrDefault()
|
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (rows.Count == 0)
|
||||||
|
return Array.Empty<WorkOrderBoardRawRow>();
|
||||||
|
|
||||||
|
var upliftByWorkOrder = await LoadUpliftAggregatesAsync(context, query, cancellationToken);
|
||||||
|
|
||||||
return rows.Select(w =>
|
return rows.Select(w =>
|
||||||
{
|
{
|
||||||
|
|
@ -129,6 +103,7 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.VendorId,
|
w.VendorId,
|
||||||
w.VendorName,
|
w.VendorName,
|
||||||
w.PrimaryDispatchStatus);
|
w.PrimaryDispatchStatus);
|
||||||
|
upliftByWorkOrder.TryGetValue(w.Id, out var uplift);
|
||||||
|
|
||||||
return new WorkOrderBoardRawRow(
|
return new WorkOrderBoardRawRow(
|
||||||
w.Id,
|
w.Id,
|
||||||
|
|
@ -171,13 +146,137 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.PrimaryDispatchId,
|
w.PrimaryDispatchId,
|
||||||
w.RowVersion,
|
w.RowVersion,
|
||||||
w.DispatchRowVersion,
|
w.DispatchRowVersion,
|
||||||
w.PendingUpliftCount,
|
uplift?.PendingCount ?? 0,
|
||||||
w.HasUplift,
|
uplift?.HasUplift ?? false,
|
||||||
w.PrimaryUpliftStatus,
|
uplift?.PrimaryStatus,
|
||||||
w.PrimaryUpliftAmount,
|
uplift?.PrimaryAmount,
|
||||||
w.PrimaryDispatchStatus,
|
w.PrimaryDispatchStatus,
|
||||||
|
w.AvetaRequired,
|
||||||
|
w.HasAvetaDocument,
|
||||||
|
w.OriginalDate,
|
||||||
|
w.OriginalWeek,
|
||||||
isUnscheduled);
|
isUnscheduled);
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SH-188: one round-trip for live uplifts on WOs in <paramref name="workOrders"/>
|
||||||
|
/// (primary WorkOrderId and multi-WO DispatchWorkOrders links). Uses the same
|
||||||
|
/// IQueryable as a SQL subquery so large boards do not hit the 2100-parameter cap.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<Dictionary<int, BoardUpliftAgg>> LoadUpliftAggregatesAsync(
|
||||||
|
ApplicationDbContext context,
|
||||||
|
IQueryable<WorkOrder> workOrders,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var workOrderIds = workOrders.Select(w => w.Id);
|
||||||
|
|
||||||
|
var live = context.DispatchUpliftRequests
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(u =>
|
||||||
|
(u.IsDeleted == null || u.IsDeleted == false)
|
||||||
|
&& u.Dispatch != null
|
||||||
|
&& (u.Dispatch.IsDeleted == null || u.Dispatch.IsDeleted == false));
|
||||||
|
|
||||||
|
var fromPrimary = live
|
||||||
|
.Where(u =>
|
||||||
|
u.Dispatch!.WorkOrderId != null
|
||||||
|
&& workOrderIds.Contains(u.Dispatch.WorkOrderId.Value))
|
||||||
|
.Select(u => new
|
||||||
|
{
|
||||||
|
UpliftId = u.Id,
|
||||||
|
WorkOrderId = u.Dispatch!.WorkOrderId!.Value,
|
||||||
|
u.Status,
|
||||||
|
u.RequestedNTE,
|
||||||
|
u.CreatedDate
|
||||||
|
});
|
||||||
|
|
||||||
|
var fromLinks = live
|
||||||
|
.SelectMany(
|
||||||
|
u => u.Dispatch!.DispatchWorkOrders!
|
||||||
|
.Where(link => workOrderIds.Contains(link.WorkOrderId)),
|
||||||
|
(u, link) => new
|
||||||
|
{
|
||||||
|
UpliftId = u.Id,
|
||||||
|
WorkOrderId = link.WorkOrderId,
|
||||||
|
u.Status,
|
||||||
|
u.RequestedNTE,
|
||||||
|
u.CreatedDate
|
||||||
|
});
|
||||||
|
|
||||||
|
var links = await fromPrimary.Concat(fromLinks).ToListAsync(cancellationToken);
|
||||||
|
return AggregateUpliftLinks(links.Select(l => new UpliftLinkRow(
|
||||||
|
l.UpliftId,
|
||||||
|
l.WorkOrderId,
|
||||||
|
l.Status,
|
||||||
|
l.RequestedNTE,
|
||||||
|
l.CreatedDate)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<int, BoardUpliftAgg> AggregateUpliftLinks(IEnumerable<UpliftLinkRow> links)
|
||||||
|
{
|
||||||
|
var seen = new HashSet<(int UpliftId, int WorkOrderId)>();
|
||||||
|
var byWorkOrder = new Dictionary<int, List<UpliftLinkRow>>();
|
||||||
|
|
||||||
|
foreach (var link in links)
|
||||||
|
{
|
||||||
|
if (!seen.Add((link.UpliftId, link.WorkOrderId)))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!byWorkOrder.TryGetValue(link.WorkOrderId, out var group))
|
||||||
|
{
|
||||||
|
group = new List<UpliftLinkRow>();
|
||||||
|
byWorkOrder[link.WorkOrderId] = group;
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Add(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new Dictionary<int, BoardUpliftAgg>(byWorkOrder.Count);
|
||||||
|
foreach (var (workOrderId, group) in byWorkOrder)
|
||||||
|
{
|
||||||
|
var pendingCount = 0;
|
||||||
|
UpliftLinkRow? primary = null;
|
||||||
|
foreach (var item in group)
|
||||||
|
{
|
||||||
|
if (item.Status == "Pending")
|
||||||
|
pendingCount++;
|
||||||
|
if (primary == null || IsNewer(item.CreatedDate, primary.CreatedDate))
|
||||||
|
primary = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
result[workOrderId] = new BoardUpliftAgg(
|
||||||
|
pendingCount,
|
||||||
|
HasUplift: true,
|
||||||
|
primary?.Status,
|
||||||
|
primary?.RequestedNTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNewer(DateTime? candidate, DateTime? current)
|
||||||
|
{
|
||||||
|
if (candidate == current)
|
||||||
|
return false;
|
||||||
|
if (candidate is null)
|
||||||
|
return false;
|
||||||
|
if (current is null)
|
||||||
|
return true;
|
||||||
|
return candidate.Value > current.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record UpliftLinkRow(
|
||||||
|
int UpliftId,
|
||||||
|
int WorkOrderId,
|
||||||
|
string Status,
|
||||||
|
decimal RequestedNTE,
|
||||||
|
DateTime? CreatedDate);
|
||||||
|
|
||||||
|
private sealed record BoardUpliftAgg(
|
||||||
|
int PendingCount,
|
||||||
|
bool HasUplift,
|
||||||
|
string? PrimaryStatus,
|
||||||
|
decimal? PrimaryAmount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,16 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
return query.Where(w => w.IsAddOn);
|
return query.Where(w => w.IsAddOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IQueryable<WorkOrder> ApplyAvetaOnlyFilter(
|
||||||
|
IQueryable<WorkOrder> query,
|
||||||
|
bool avetaOnly)
|
||||||
|
{
|
||||||
|
if (!avetaOnly)
|
||||||
|
return query;
|
||||||
|
|
||||||
|
return query.Where(w => w.AvetaRequired);
|
||||||
|
}
|
||||||
|
|
||||||
public static IQueryable<WorkOrder> ApplyFlagColorFilter(
|
public static IQueryable<WorkOrder> ApplyFlagColorFilter(
|
||||||
IQueryable<WorkOrder> query,
|
IQueryable<WorkOrder> query,
|
||||||
IReadOnlyList<string>? flagColors)
|
IReadOnlyList<string>? flagColors)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyRescheduledFilter(baseQuery, query.Rescheduled);
|
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyRescheduledFilter(baseQuery, query.Rescheduled);
|
||||||
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyCarriedOverFilter(baseQuery, query.CarriedOver);
|
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyCarriedOverFilter(baseQuery, query.CarriedOver);
|
||||||
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyAddOnFilter(baseQuery, query.AddOn);
|
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyAddOnFilter(baseQuery, query.AddOn);
|
||||||
|
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyAvetaOnlyFilter(baseQuery, query.AvetaOnly);
|
||||||
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyFlagColorFilter(baseQuery, query.FlagColors);
|
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyFlagColorFilter(baseQuery, query.FlagColors);
|
||||||
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyInternalOnlyFilter(baseQuery, query.InternalOnly);
|
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyInternalOnlyFilter(baseQuery, query.InternalOnly);
|
||||||
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyUpliftFilter(
|
baseQuery = WorkOrderBoardSearchFacetFilters.ApplyUpliftFilter(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Data.SeaHavenIndustries.Enums;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SeaHaven.DataServices.Helpers;
|
using SeaHaven.DataServices.Helpers;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
|
@ -15,10 +14,12 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
_context = context;
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WorkOrderBoardQueryResult> GetBoardRowsAsync(WorkOrderBoardQuery query)
|
public async Task<WorkOrderBoardQueryResult> GetBoardRowsAsync(
|
||||||
|
WorkOrderBoardQuery query,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var weekStartDate = query.WeekStart.ToDateTime(TimeOnly.MinValue);
|
var weekStartDate = query.WeekStart.ToDateTime(TimeOnly.MinValue);
|
||||||
var weekEndDate = query.WeekEnd.ToDateTime(TimeOnly.MinValue);
|
var weekEndExclusive = query.WeekEnd.AddDays(1).ToDateTime(TimeOnly.MinValue);
|
||||||
|
|
||||||
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
||||||
if (query.AccountId.HasValue)
|
if (query.AccountId.HasValue)
|
||||||
|
|
@ -29,33 +30,27 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
|
|
||||||
var scheduledBase = baseQuery.Where(w =>
|
var scheduledBase = baseQuery.Where(w =>
|
||||||
(w.ScheduledDate != null
|
(w.ScheduledDate != null
|
||||||
&& w.ScheduledDate.Value.Date >= weekStartDate.Date
|
&& w.ScheduledDate >= weekStartDate
|
||||||
&& w.ScheduledDate.Value.Date <= weekEndDate.Date)
|
&& w.ScheduledDate < weekEndExclusive)
|
||||||
|| (w.ScheduleWeekOnly == true && w.TargetWeek == query.WeekStart));
|
|| (w.ScheduleWeekOnly == true && w.TargetWeek == query.WeekStart));
|
||||||
|
|
||||||
var unscheduledBase = baseQuery.Where(w =>
|
var scheduledTotal = await scheduledBase.CountAsync(cancellationToken);
|
||||||
w.ScheduledDate == null
|
|
||||||
&& !LifecycleStatusSets.Terminal.Contains(w.LifecycleStatus));
|
|
||||||
|
|
||||||
var scheduledTotal = await scheduledBase.CountAsync();
|
|
||||||
|
|
||||||
var normalizedSearch = WorkOrderBoardSearchFilter.NormalizeSearch(query.Search);
|
var normalizedSearch = WorkOrderBoardSearchFilter.NormalizeSearch(query.Search);
|
||||||
if (normalizedSearch != null)
|
if (normalizedSearch != null)
|
||||||
{
|
|
||||||
scheduledBase = WorkOrderBoardSearchFilter.Apply(scheduledBase, normalizedSearch);
|
scheduledBase = WorkOrderBoardSearchFilter.Apply(scheduledBase, normalizedSearch);
|
||||||
unscheduledBase = WorkOrderBoardSearchFilter.Apply(unscheduledBase, normalizedSearch);
|
|
||||||
}
|
|
||||||
|
|
||||||
var scheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync(
|
var scheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync(
|
||||||
_context,
|
_context,
|
||||||
scheduledBase.OrderBy(w => w.ScheduledDate).ThenBy(w => w.InternalWONumber),
|
scheduledBase.OrderBy(w => w.ScheduledDate).ThenBy(w => w.InternalWONumber),
|
||||||
isUnscheduled: false);
|
isUnscheduled: false,
|
||||||
var unscheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync(
|
cancellationToken);
|
||||||
_context,
|
|
||||||
unscheduledBase.OrderBy(w => w.InternalWONumber),
|
|
||||||
isUnscheduled: true);
|
|
||||||
|
|
||||||
return new WorkOrderBoardQueryResult(scheduledRows, unscheduledRows, scheduledTotal);
|
// Weekly board is scheduled-in-week only. Undated rows belong on GET /board/search.
|
||||||
|
return new WorkOrderBoardQueryResult(
|
||||||
|
scheduledRows,
|
||||||
|
Array.Empty<WorkOrderBoardRawRow>(),
|
||||||
|
scheduledTotal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<DispatcherLookupRow>> GetDispatcherLookupsAsync()
|
public async Task<IReadOnlyList<DispatcherLookupRow>> GetDispatcherLookupsAsync()
|
||||||
|
|
@ -74,14 +69,18 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WorkOrderBoardRawRow?> GetBoardRowByIdAsync(int workOrderId, int? accountId = null)
|
public async Task<WorkOrderBoardRawRow?> GetBoardRowByIdAsync(
|
||||||
|
int workOrderId,
|
||||||
|
int? accountId = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var query = WorkOrderBoardQueryFilters.ApplyBaseScope(
|
var query = WorkOrderBoardQueryFilters.ApplyBaseScope(
|
||||||
_context.workOrders.AsNoTracking().Where(w => w.Id == workOrderId));
|
_context.workOrders.AsNoTracking().Where(w => w.Id == workOrderId));
|
||||||
if (accountId.HasValue)
|
if (accountId.HasValue)
|
||||||
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
|
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
|
||||||
|
|
||||||
var rows = await WorkOrderBoardProjection.ProjectRowsAsync(_context, query, isUnscheduled: false);
|
var rows = await WorkOrderBoardProjection.ProjectRowsAsync(
|
||||||
|
_context, query, isUnscheduled: false, cancellationToken);
|
||||||
return rows.FirstOrDefault();
|
return rows.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,14 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
{
|
{
|
||||||
public interface IWorkOrderBoardDataService
|
public interface IWorkOrderBoardDataService
|
||||||
{
|
{
|
||||||
Task<WorkOrderBoardQueryResult> GetBoardRowsAsync(WorkOrderBoardQuery query);
|
Task<WorkOrderBoardQueryResult> GetBoardRowsAsync(
|
||||||
|
WorkOrderBoardQuery query,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
Task<IReadOnlyList<DispatcherLookupRow>> GetDispatcherLookupsAsync();
|
Task<IReadOnlyList<DispatcherLookupRow>> GetDispatcherLookupsAsync();
|
||||||
Task<WorkOrderBoardRawRow?> GetBoardRowByIdAsync(int workOrderId, int? accountId = null);
|
Task<WorkOrderBoardRawRow?> GetBoardRowByIdAsync(
|
||||||
|
int workOrderId,
|
||||||
|
int? accountId = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
Task<bool> InternalWoNumberExistsAsync(string normalizedWoNumber, int excludeWorkOrderId);
|
Task<bool> InternalWoNumberExistsAsync(string normalizedWoNumber, int excludeWorkOrderId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
bool Rescheduled,
|
bool Rescheduled,
|
||||||
bool CarriedOver,
|
bool CarriedOver,
|
||||||
bool AddOn,
|
bool AddOn,
|
||||||
|
bool AvetaOnly,
|
||||||
IReadOnlyList<string>? FlagColors,
|
IReadOnlyList<string>? FlagColors,
|
||||||
bool InternalOnly,
|
bool InternalOnly,
|
||||||
bool HasUplift,
|
bool HasUplift,
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,10 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
string? PrimaryUpliftStatus,
|
string? PrimaryUpliftStatus,
|
||||||
decimal? PrimaryUpliftAmount,
|
decimal? PrimaryUpliftAmount,
|
||||||
string? PrimaryDispatchStatus,
|
string? PrimaryDispatchStatus,
|
||||||
|
bool AvetaRequired,
|
||||||
|
bool HasAvetaDocument,
|
||||||
|
DateOnly? OriginalDate,
|
||||||
|
DateOnly? OriginalWeek,
|
||||||
bool IsUnscheduled);
|
bool IsUnscheduled);
|
||||||
|
|
||||||
public record WorkOrderBoardQueryResult(
|
public record WorkOrderBoardQueryResult(
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ namespace SeaHaven.Services.DTOs
|
||||||
public string? Search { get; set; }
|
public string? Search { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scheduled-in-week counts for GET /board. <see cref="Total"/> is the week
|
||||||
|
/// match before the bar search filter; <see cref="Returned"/> is after it.
|
||||||
|
/// Undated work orders are not included.
|
||||||
|
/// </summary>
|
||||||
public class WorkOrderBoardCountsDto
|
public class WorkOrderBoardCountsDto
|
||||||
{
|
{
|
||||||
public int Returned { get; set; }
|
public int Returned { get; set; }
|
||||||
|
|
@ -36,6 +41,10 @@ namespace SeaHaven.Services.DTOs
|
||||||
public int RescheduleCount { get; set; }
|
public int RescheduleCount { get; set; }
|
||||||
public int CarriedOver { get; set; }
|
public int CarriedOver { get; set; }
|
||||||
public bool IsAddOn { get; set; }
|
public bool IsAddOn { get; set; }
|
||||||
|
public bool AvetaRequired { get; set; }
|
||||||
|
public bool HasAvetaDocument { get; set; }
|
||||||
|
public DateOnly? OriginalDate { get; set; }
|
||||||
|
public DateOnly? OriginalWeek { get; set; }
|
||||||
public WorkOrderType? WorkOrderType { get; set; }
|
public WorkOrderType? WorkOrderType { get; set; }
|
||||||
public bool IsPastDue { get; set; }
|
public bool IsPastDue { get; set; }
|
||||||
public string? SiteCode { get; set; }
|
public string? SiteCode { get; set; }
|
||||||
|
|
@ -111,6 +120,10 @@ namespace SeaHaven.Services.DTOs
|
||||||
public DateOnly WeekStart { get; set; }
|
public DateOnly WeekStart { get; set; }
|
||||||
public DateOnly WeekEnd { get; set; }
|
public DateOnly WeekEnd { get; set; }
|
||||||
public WorkOrderBoardCountsDto Counts { get; set; } = new();
|
public WorkOrderBoardCountsDto Counts { get; set; } = new();
|
||||||
|
/// <summary>
|
||||||
|
/// Always empty on the weekly board. Undated rows are served by GET /board/search.
|
||||||
|
/// Kept so existing clients can keep reading the field.
|
||||||
|
/// </summary>
|
||||||
public List<WorkOrderBoardRowDto> Unscheduled { get; set; } = new();
|
public List<WorkOrderBoardRowDto> Unscheduled { get; set; } = new();
|
||||||
public List<WorkOrderBoardRowDto> Scheduled { get; set; } = new();
|
public List<WorkOrderBoardRowDto> Scheduled { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ namespace SeaHaven.Services.DTOs
|
||||||
public string? PrimaryService { get; set; }
|
public string? PrimaryService { get; set; }
|
||||||
public List<string>? ExtraServices { get; set; }
|
public List<string>? ExtraServices { get; set; }
|
||||||
public string? ServiceNotes { get; set; }
|
public string? ServiceNotes { get; set; }
|
||||||
|
/// <summary>Per-WO Aveta compliance. Omit or false = not required.</summary>
|
||||||
|
public bool? AvetaRequired { get; set; }
|
||||||
public int? LocationId { get; set; }
|
public int? LocationId { get; set; }
|
||||||
public DateTime? DueDate { get; set; }
|
public DateTime? DueDate { get; set; }
|
||||||
public string? AssignTo { get; set; }
|
public string? AssignTo { get; set; }
|
||||||
|
|
@ -73,6 +75,8 @@ namespace SeaHaven.Services.DTOs
|
||||||
public bool Rescheduled { get; set; }
|
public bool Rescheduled { get; set; }
|
||||||
public bool CarriedOver { get; set; }
|
public bool CarriedOver { get; set; }
|
||||||
public bool AddOn { get; set; }
|
public bool AddOn { get; set; }
|
||||||
|
/// <summary>When true, only rows with AvetaRequired.</summary>
|
||||||
|
public bool AvetaOnly { get; set; }
|
||||||
public List<string>? FlagColors { get; set; }
|
public List<string>? FlagColors { get; set; }
|
||||||
public bool InternalOnly { get; set; }
|
public bool InternalOnly { get; set; }
|
||||||
public bool HasUplift { get; set; }
|
public bool HasUplift { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@ namespace SeaHaven.Services.DTOs
|
||||||
public string? Trade { get; set; }
|
public string? Trade { get; set; }
|
||||||
public string? Problem { get; set; }
|
public string? Problem { get; set; }
|
||||||
public string? SubTrade { get; set; }
|
public string? SubTrade { get; set; }
|
||||||
public DateOnly? OriginalWeek { get; set; }
|
|
||||||
public DateOnly? OriginalDate { get; set; }
|
|
||||||
public string? VendorNotes { get; set; }
|
public string? VendorNotes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ namespace SeaHaven.Services.Helpers
|
||||||
public const string ExtraServices = "extraServices";
|
public const string ExtraServices = "extraServices";
|
||||||
public const string AdditionalContacts = "additionalContacts";
|
public const string AdditionalContacts = "additionalContacts";
|
||||||
public const string FlagColor = "flagColor";
|
public const string FlagColor = "flagColor";
|
||||||
|
public const string AvetaRequired = "avetaRequired";
|
||||||
public const string RescheduleCount = "RescheduleCount";
|
public const string RescheduleCount = "RescheduleCount";
|
||||||
|
|
||||||
private static readonly HashSet<string> DispatchFields = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> DispatchFields = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
|
@ -50,7 +51,8 @@ namespace SeaHaven.Services.Helpers
|
||||||
ServiceNotes,
|
ServiceNotes,
|
||||||
ExtraServices,
|
ExtraServices,
|
||||||
AdditionalContacts,
|
AdditionalContacts,
|
||||||
FlagColor
|
FlagColor,
|
||||||
|
AvetaRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
public static bool IsEditable(string field) => Canonicalize(field) != null;
|
public static bool IsEditable(string field) => Canonicalize(field) != null;
|
||||||
|
|
@ -93,6 +95,7 @@ namespace SeaHaven.Services.Helpers
|
||||||
ExtraServices => "ExtraServices",
|
ExtraServices => "ExtraServices",
|
||||||
AdditionalContacts => "AdditionalContacts",
|
AdditionalContacts => "AdditionalContacts",
|
||||||
FlagColor => "FlagColor",
|
FlagColor => "FlagColor",
|
||||||
|
AvetaRequired => "AvetaRequired",
|
||||||
_ => canonical
|
_ => canonical
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,13 @@ namespace SeaHaven.Services.Helpers
|
||||||
return false;
|
return false;
|
||||||
if (!IsCoreLocked(status))
|
if (!IsCoreLocked(status))
|
||||||
return true;
|
return true;
|
||||||
return (category ?? WorkOrderMediaCategory.Extra) == WorkOrderMediaCategory.Extra;
|
return IsExtraDocsFamily(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsExtraDocsFamily(WorkOrderMediaCategory? category)
|
||||||
|
{
|
||||||
|
var resolved = category ?? WorkOrderMediaCategory.Extra;
|
||||||
|
return resolved == WorkOrderMediaCategory.Extra || resolved == WorkOrderMediaCategory.Aveta;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool ShouldBlockStatusChangeWhenPastDue(string field, bool isPastDue)
|
public static bool ShouldBlockStatusChangeWhenPastDue(string field, bool isPastDue)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
public class WorkOrderAdvancedSearchService : IWorkOrderAdvancedSearchService
|
public class WorkOrderAdvancedSearchService : IWorkOrderAdvancedSearchService
|
||||||
{
|
{
|
||||||
private const int DefaultPageSize = 100;
|
private const int DefaultPageSize = 100;
|
||||||
private const int MaxPageSize = 100;
|
private const int MaxPageSize = 200;
|
||||||
|
|
||||||
private readonly IWorkOrderAdvancedSearchDataService _searchDataService;
|
private readonly IWorkOrderAdvancedSearchDataService _searchDataService;
|
||||||
private readonly IWorkOrderAccountResolver _accountResolver;
|
private readonly IWorkOrderAccountResolver _accountResolver;
|
||||||
|
|
@ -55,6 +55,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
query.Rescheduled,
|
query.Rescheduled,
|
||||||
query.CarriedOver,
|
query.CarriedOver,
|
||||||
query.AddOn,
|
query.AddOn,
|
||||||
|
query.AvetaOnly,
|
||||||
query.FlagColors,
|
query.FlagColors,
|
||||||
query.InternalOnly,
|
query.InternalOnly,
|
||||||
query.HasUplift,
|
query.HasUplift,
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
Trade = primaryService,
|
Trade = primaryService,
|
||||||
ExtraServices = extraServicesJson,
|
ExtraServices = extraServicesJson,
|
||||||
ServiceNotes = serviceNotes,
|
ServiceNotes = serviceNotes,
|
||||||
|
AvetaRequired = request.AvetaRequired == true,
|
||||||
PocName = pocName,
|
PocName = pocName,
|
||||||
PocPhone = pocPhone,
|
PocPhone = pocPhone,
|
||||||
PocNotes = pocNotes,
|
PocNotes = pocNotes,
|
||||||
|
|
@ -130,6 +131,8 @@ namespace SeaHaven.Services.Implementation
|
||||||
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ExtraServices", "", workOrder.ExtraServices));
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ExtraServices", "", workOrder.ExtraServices));
|
||||||
if (workOrder.ServiceNotes != null)
|
if (workOrder.ServiceNotes != null)
|
||||||
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ServiceNotes", "", workOrder.ServiceNotes));
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ServiceNotes", "", workOrder.ServiceNotes));
|
||||||
|
if (workOrder.AvetaRequired)
|
||||||
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("AvetaRequired", "", bool.TrueString));
|
||||||
if (workOrder.PocName != null)
|
if (workOrder.PocName != null)
|
||||||
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("PocName", "", workOrder.PocName));
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("PocName", "", workOrder.PocName));
|
||||||
if (workOrder.PocPhone != null)
|
if (workOrder.PocPhone != null)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ namespace SeaHaven.Services.Implementation
|
||||||
public async Task<WorkOrderBoardResponseDto> GetBoardAsync(
|
public async Task<WorkOrderBoardResponseDto> GetBoardAsync(
|
||||||
WorkOrderBoardQueryDto query,
|
WorkOrderBoardQueryDto query,
|
||||||
ClaimsPrincipal user,
|
ClaimsPrincipal user,
|
||||||
string? currentUserId)
|
string? currentUserId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var accountId = _accountResolver.ResolveAccountFilter(user);
|
var accountId = _accountResolver.ResolveAccountFilter(user);
|
||||||
var weekStart = query.WeekStart;
|
var weekStart = query.WeekStart;
|
||||||
|
|
@ -45,7 +46,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
currentUserId,
|
currentUserId,
|
||||||
accountId);
|
accountId);
|
||||||
|
|
||||||
var result = await _boardDataService.GetBoardRowsAsync(dataQuery);
|
var result = await _boardDataService.GetBoardRowsAsync(dataQuery, cancellationToken);
|
||||||
var utcNow = DateTime.UtcNow;
|
var utcNow = DateTime.UtcNow;
|
||||||
|
|
||||||
return new WorkOrderBoardResponseDto
|
return new WorkOrderBoardResponseDto
|
||||||
|
|
@ -99,6 +100,10 @@ namespace SeaHaven.Services.Implementation
|
||||||
RescheduleCount = row.RescheduleCount,
|
RescheduleCount = row.RescheduleCount,
|
||||||
CarriedOver = row.CarriedOver,
|
CarriedOver = row.CarriedOver,
|
||||||
IsAddOn = row.IsAddOn,
|
IsAddOn = row.IsAddOn,
|
||||||
|
AvetaRequired = row.AvetaRequired,
|
||||||
|
HasAvetaDocument = row.HasAvetaDocument,
|
||||||
|
OriginalDate = row.OriginalDate,
|
||||||
|
OriginalWeek = row.OriginalWeek,
|
||||||
WorkOrderType = row.WorkOrderType,
|
WorkOrderType = row.WorkOrderType,
|
||||||
IsPastDue = WorkOrderDerivedFields.IsPastDue(row.ScheduledDate, row.LifecycleStatus, utcNow),
|
IsPastDue = WorkOrderDerivedFields.IsPastDue(row.ScheduledDate, row.LifecycleStatus, utcNow),
|
||||||
SiteCode = row.SiteCode,
|
SiteCode = row.SiteCode,
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
WorkOrderBoardFieldNames.ExtraServices => new List<FieldChange> { ApplyExtraServices(workOrder, value, auditField) },
|
WorkOrderBoardFieldNames.ExtraServices => new List<FieldChange> { ApplyExtraServices(workOrder, value, auditField) },
|
||||||
WorkOrderBoardFieldNames.AdditionalContacts => new List<FieldChange> { ApplyAdditionalContacts(workOrder, value, auditField) },
|
WorkOrderBoardFieldNames.AdditionalContacts => new List<FieldChange> { ApplyAdditionalContacts(workOrder, value, auditField) },
|
||||||
WorkOrderBoardFieldNames.FlagColor => new List<FieldChange> { ApplyFlagColor(workOrder, value, auditField) },
|
WorkOrderBoardFieldNames.FlagColor => new List<FieldChange> { ApplyFlagColor(workOrder, value, auditField) },
|
||||||
|
WorkOrderBoardFieldNames.AvetaRequired => new List<FieldChange> { ApplyAvetaRequired(workOrder, value, auditField) },
|
||||||
_ => throw new WorkOrderBoardValidationException("InvalidField", $"Field '{field}' is not editable.")
|
_ => throw new WorkOrderBoardValidationException("InvalidField", $"Field '{field}' is not editable.")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -679,6 +680,15 @@ namespace SeaHaven.Services.Implementation
|
||||||
return FieldChange.ForField(auditField, old, parsed.ToString());
|
return FieldChange.ForField(auditField, old, parsed.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static FieldChange ApplyAvetaRequired(WorkOrder workOrder, string? value, string auditField)
|
||||||
|
{
|
||||||
|
return ApplyBoolField(
|
||||||
|
value,
|
||||||
|
auditField,
|
||||||
|
v => workOrder.AvetaRequired = v == true,
|
||||||
|
() => workOrder.AvetaRequired);
|
||||||
|
}
|
||||||
|
|
||||||
private static FieldChange ApplyFlagColor(WorkOrder workOrder, string? value, string auditField)
|
private static FieldChange ApplyFlagColor(WorkOrder workOrder, string? value, string auditField)
|
||||||
{
|
{
|
||||||
if (!WorkOrderFlagColors.TryNormalize(value, out var canonical))
|
if (!WorkOrderFlagColors.TryNormalize(value, out var canonical))
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,8 @@ namespace SeaHaven.Services.Implementation
|
||||||
RescheduleCount = row.RescheduleCount,
|
RescheduleCount = row.RescheduleCount,
|
||||||
CarriedOver = row.CarriedOver,
|
CarriedOver = row.CarriedOver,
|
||||||
IsAddOn = row.IsAddOn,
|
IsAddOn = row.IsAddOn,
|
||||||
|
AvetaRequired = row.AvetaRequired,
|
||||||
|
HasAvetaDocument = row.HasAvetaDocument,
|
||||||
WorkOrderType = row.WorkOrderType,
|
WorkOrderType = row.WorkOrderType,
|
||||||
IsPastDue = row.IsPastDue,
|
IsPastDue = row.IsPastDue,
|
||||||
SiteCode = row.SiteCode,
|
SiteCode = row.SiteCode,
|
||||||
|
|
@ -137,8 +139,8 @@ namespace SeaHaven.Services.Implementation
|
||||||
Trade = extended?.Trade,
|
Trade = extended?.Trade,
|
||||||
Problem = extended?.Problem,
|
Problem = extended?.Problem,
|
||||||
SubTrade = extended?.SubTrade,
|
SubTrade = extended?.SubTrade,
|
||||||
OriginalWeek = extended?.OriginalWeek,
|
OriginalWeek = row.OriginalWeek ?? extended?.OriginalWeek,
|
||||||
OriginalDate = extended?.OriginalDate,
|
OriginalDate = row.OriginalDate ?? extended?.OriginalDate,
|
||||||
CompletedDate = extended?.CompletedDate,
|
CompletedDate = extended?.CompletedDate,
|
||||||
VendorNotes = extended?.VendorNotes
|
VendorNotes = extended?.VendorNotes
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ namespace SeaHaven.Services.Interfaces
|
||||||
Task<WorkOrderBoardResponseDto> GetBoardAsync(
|
Task<WorkOrderBoardResponseDto> GetBoardAsync(
|
||||||
WorkOrderBoardQueryDto query,
|
WorkOrderBoardQueryDto query,
|
||||||
ClaimsPrincipal user,
|
ClaimsPrincipal user,
|
||||||
string? currentUserId);
|
string? currentUserId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
Task<IReadOnlyList<DispatcherLookupDto>> GetDispatcherLookupsAsync();
|
Task<IReadOnlyList<DispatcherLookupDto>> GetDispatcherLookupsAsync();
|
||||||
Task<WorkOrderBoardRowDto?> GetBoardRowAsync(int workOrderId, ClaimsPrincipal user);
|
Task<WorkOrderBoardRowDto?> GetBoardRowAsync(int workOrderId, ClaimsPrincipal user);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,45 @@ public class WorkOrderBoardCreateServiceTests
|
||||||
Assert.True(wo.IsAddOn);
|
Assert.True(wo.IsAddOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Create_AvetaRequiredTrue_PersistsAndReturns()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
|
||||||
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||||
|
{
|
||||||
|
WorkOrderType = WorkOrderType.PM,
|
||||||
|
SiteCode = "BK5",
|
||||||
|
AvetaRequired = true
|
||||||
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
||||||
|
|
||||||
|
Assert.True(result.AvetaRequired);
|
||||||
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
||||||
|
Assert.True(wo.AvetaRequired);
|
||||||
|
Assert.Contains(
|
||||||
|
await context.WorkOrderAuditLogs.ToListAsync(),
|
||||||
|
l => l.FieldName == "AvetaRequired" && l.Action == "FieldChanged");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Create_AvetaRequiredOmitted_DefaultsFalse()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
|
||||||
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||||
|
{
|
||||||
|
WorkOrderType = WorkOrderType.PM,
|
||||||
|
SiteCode = "BK5"
|
||||||
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
||||||
|
|
||||||
|
Assert.False(result.AvetaRequired);
|
||||||
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
||||||
|
Assert.False(wo.AvetaRequired);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
await context.WorkOrderAuditLogs.ToListAsync(),
|
||||||
|
l => l.FieldName == "AvetaRequired");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Create_WorkOrderTypeAddOn_ThrowsValidation()
|
public async Task Create_WorkOrderTypeAddOn_ThrowsValidation()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,12 @@ public class WorkOrderBoardMutationRulesTests
|
||||||
LifecycleStatus.Completed, WorkOrderMediaCategory.Before));
|
LifecycleStatus.Completed, WorkOrderMediaCategory.Before));
|
||||||
Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia(
|
Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia(
|
||||||
LifecycleStatus.Completed, WorkOrderMediaCategory.After));
|
LifecycleStatus.Completed, WorkOrderMediaCategory.After));
|
||||||
|
Assert.True(WorkOrderBoardMutationRules.CanMutateExtraMedia(
|
||||||
|
LifecycleStatus.Completed, WorkOrderMediaCategory.Aveta));
|
||||||
Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia(
|
Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia(
|
||||||
LifecycleStatus.Canceled, WorkOrderMediaCategory.Extra));
|
LifecycleStatus.Canceled, WorkOrderMediaCategory.Extra));
|
||||||
|
Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia(
|
||||||
|
LifecycleStatus.Canceled, WorkOrderMediaCategory.Aveta));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -141,8 +141,7 @@ public class WorkOrderBoardSearchCountsTests
|
||||||
Assert.Equal(2, result.ScheduledTotalBeforeSearch);
|
Assert.Equal(2, result.ScheduledTotalBeforeSearch);
|
||||||
Assert.Single(result.ScheduledRows);
|
Assert.Single(result.ScheduledRows);
|
||||||
Assert.Equal("BK5", result.ScheduledRows[0].SiteCode);
|
Assert.Equal("BK5", result.ScheduledRows[0].SiteCode);
|
||||||
Assert.Single(result.UnscheduledRows);
|
Assert.Empty(result.UnscheduledRows);
|
||||||
Assert.Equal("BK5", result.UnscheduledRows[0].SiteCode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,7 +241,180 @@ public class WorkOrderAdvancedSearchServiceTests
|
||||||
PageSize = 500
|
PageSize = 500
|
||||||
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
||||||
|
|
||||||
Assert.Equal(100, result.PageSize);
|
Assert.Equal(200, result.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SearchAsync_AcceptsPageSize200()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var dataService = new WorkOrderAdvancedSearchDataService(context);
|
||||||
|
var service = new SeaHaven.Services.Implementation.WorkOrderAdvancedSearchService(dataService, WorkOrderAccountTestHelpers.Resolver(context));
|
||||||
|
|
||||||
|
var result = await service.SearchAsync(new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto
|
||||||
|
{
|
||||||
|
DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.ThisWeek,
|
||||||
|
PageSize = 200
|
||||||
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
||||||
|
|
||||||
|
Assert.Equal(200, result.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(25)]
|
||||||
|
[InlineData(50)]
|
||||||
|
[InlineData(200)]
|
||||||
|
public async Task SearchAsync_UnassignedDispatcher_AcceptsBoardPagerPageSizes(int pageSize)
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var dataService = new WorkOrderAdvancedSearchDataService(context);
|
||||||
|
var service = new SeaHaven.Services.Implementation.WorkOrderAdvancedSearchService(
|
||||||
|
dataService,
|
||||||
|
WorkOrderAccountTestHelpers.Resolver(context));
|
||||||
|
|
||||||
|
var result = await service.SearchAsync(new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto
|
||||||
|
{
|
||||||
|
DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.Custom,
|
||||||
|
DateFrom = new DateOnly(2000, 1, 1),
|
||||||
|
DateTo = new DateOnly(2099, 12, 31),
|
||||||
|
Dispatchers = new List<string> { "__unassigned__" },
|
||||||
|
Page = 0,
|
||||||
|
PageSize = pageSize
|
||||||
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
||||||
|
|
||||||
|
Assert.Equal(pageSize, result.PageSize);
|
||||||
|
Assert.Equal(0, result.Page);
|
||||||
|
Assert.Equal(0, result.PageBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SearchAsync_UnassignedDispatcher_WideCustomRange_IncludesDatedOutsideWeekAndUndated()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
|
||||||
|
context.workOrders.AddRange(
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
AssignTo = null,
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 23),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
InternalWONumber = "10000000002",
|
||||||
|
AssignTo = null,
|
||||||
|
ScheduledDate = new DateTime(2026, 8, 4),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
InternalWONumber = "10000000003",
|
||||||
|
AssignTo = "",
|
||||||
|
ScheduledDate = null,
|
||||||
|
LifecycleStatus = LifecycleStatus.Incomplete
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 4,
|
||||||
|
InternalWONumber = "10000000004",
|
||||||
|
AssignTo = "user-a",
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 24),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var dataService = new WorkOrderAdvancedSearchDataService(context);
|
||||||
|
var service = new SeaHaven.Services.Implementation.WorkOrderAdvancedSearchService(
|
||||||
|
dataService,
|
||||||
|
WorkOrderAccountTestHelpers.Resolver(context));
|
||||||
|
|
||||||
|
var result = await service.SearchAsync(new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto
|
||||||
|
{
|
||||||
|
DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.Custom,
|
||||||
|
DateFrom = new DateOnly(2000, 1, 1),
|
||||||
|
DateTo = new DateOnly(2099, 12, 31),
|
||||||
|
Dispatchers = new List<string> { "__unassigned__" },
|
||||||
|
Page = 0,
|
||||||
|
PageSize = 50
|
||||||
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
||||||
|
|
||||||
|
Assert.Equal(3, result.TotalCount);
|
||||||
|
Assert.Equal(50, result.PageSize);
|
||||||
|
Assert.Contains(result.Items, i => i.Id == 1 && i.DispatcherId == null);
|
||||||
|
Assert.Contains(result.Items, i => i.Id == 2 && i.DispatcherId == null);
|
||||||
|
Assert.Contains(result.Items, i => i.Id == 3);
|
||||||
|
Assert.DoesNotContain(result.Items, i => i.Id == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SearchAsync_UnassignedDispatcher_PaginatesWideCustomRange()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
|
||||||
|
context.workOrders.AddRange(
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
AssignTo = null,
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 23),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
InternalWONumber = "10000000002",
|
||||||
|
AssignTo = null,
|
||||||
|
ScheduledDate = new DateTime(2026, 8, 4),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
InternalWONumber = "10000000003",
|
||||||
|
AssignTo = null,
|
||||||
|
ScheduledDate = null,
|
||||||
|
LifecycleStatus = LifecycleStatus.Incomplete
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var dataService = new WorkOrderAdvancedSearchDataService(context);
|
||||||
|
var service = new SeaHaven.Services.Implementation.WorkOrderAdvancedSearchService(
|
||||||
|
dataService,
|
||||||
|
WorkOrderAccountTestHelpers.Resolver(context));
|
||||||
|
|
||||||
|
var page0 = await service.SearchAsync(new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto
|
||||||
|
{
|
||||||
|
DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.Custom,
|
||||||
|
DateFrom = new DateOnly(2000, 1, 1),
|
||||||
|
DateTo = new DateOnly(2099, 12, 31),
|
||||||
|
Dispatchers = new List<string> { "__unassigned__" },
|
||||||
|
Page = 0,
|
||||||
|
PageSize = 2
|
||||||
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
||||||
|
|
||||||
|
var page1 = await service.SearchAsync(new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto
|
||||||
|
{
|
||||||
|
DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.Custom,
|
||||||
|
DateFrom = new DateOnly(2000, 1, 1),
|
||||||
|
DateTo = new DateOnly(2099, 12, 31),
|
||||||
|
Dispatchers = new List<string> { "__unassigned__" },
|
||||||
|
Page = 1,
|
||||||
|
PageSize = 2
|
||||||
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
||||||
|
|
||||||
|
Assert.Equal(3, page0.TotalCount);
|
||||||
|
Assert.Equal(2, page0.PageSize);
|
||||||
|
Assert.Equal(2, page0.Items.Count());
|
||||||
|
Assert.Equal(3, page1.TotalCount);
|
||||||
|
Assert.Single(page1.Items);
|
||||||
|
Assert.Empty(page0.Items.Select(i => i.Id).Intersect(page1.Items.Select(i => i.Id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -829,4 +1001,51 @@ public class WorkOrderAdvancedSearchServiceTests
|
||||||
Assert.Equal(1, result.TotalCount);
|
Assert.Equal(1, result.TotalCount);
|
||||||
Assert.Equal(1, result.Items.Single().Id);
|
Assert.Equal(1, result.Items.Single().Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SearchAsync_AvetaOnly_ReturnsFlaggedRows()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
|
||||||
|
context.workOrders.AddRange(
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
AvetaRequired = true,
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 23),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
AvetaRequired = false,
|
||||||
|
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,
|
||||||
|
WorkOrderAccountTestHelpers.Resolver(context));
|
||||||
|
|
||||||
|
var result = await service.SearchAsync(
|
||||||
|
new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto
|
||||||
|
{
|
||||||
|
DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.Custom,
|
||||||
|
DateFrom = weekStart,
|
||||||
|
DateTo = weekStart.AddDays(6),
|
||||||
|
AvetaOnly = true,
|
||||||
|
},
|
||||||
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
||||||
|
null);
|
||||||
|
|
||||||
|
Assert.Equal(1, result.TotalCount);
|
||||||
|
Assert.Equal(1, result.Items.Single().Id);
|
||||||
|
Assert.True(result.Items.Single().AvetaRequired);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ public class WorkOrderBoardDataServiceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetBoardRows_ReturnsScheduledInWeekAndUnscheduled()
|
public async Task GetBoardRows_ReturnsScheduledInWeekAndOmitsUnscheduledDump()
|
||||||
{
|
{
|
||||||
await using var context = CreateContext();
|
await using var context = CreateContext();
|
||||||
var weekStart = new DateOnly(2026, 6, 22);
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
|
@ -74,11 +74,56 @@ public class WorkOrderBoardDataServiceTests
|
||||||
Assert.Single(result.ScheduledRows);
|
Assert.Single(result.ScheduledRows);
|
||||||
Assert.Equal(1, result.ScheduledRows[0].Id);
|
Assert.Equal(1, result.ScheduledRows[0].Id);
|
||||||
Assert.Equal("Acme HVAC", result.ScheduledRows[0].VendorName);
|
Assert.Equal("Acme HVAC", result.ScheduledRows[0].VendorName);
|
||||||
Assert.Single(result.UnscheduledRows);
|
Assert.Empty(result.UnscheduledRows);
|
||||||
Assert.Equal(2, result.UnscheduledRows[0].Id);
|
|
||||||
Assert.Equal(1, result.ScheduledTotalBeforeSearch);
|
Assert.Equal(1, result.ScheduledTotalBeforeSearch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardRows_DifferentWeeksReturnDifferentScheduledSets()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekA = new DateOnly(2026, 6, 22);
|
||||||
|
var weekB = new DateOnly(2026, 6, 29);
|
||||||
|
|
||||||
|
context.workOrders.AddRange(
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 23),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
InternalWONumber = "10000000002",
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 30),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
InternalWONumber = "10000000003",
|
||||||
|
ScheduledDate = null,
|
||||||
|
LifecycleStatus = LifecycleStatus.Incomplete
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new WorkOrderBoardDataService(context);
|
||||||
|
var resultA = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||||
|
weekA, weekA.AddDays(4), null, false, null, false, null, null));
|
||||||
|
var resultB = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||||
|
weekB, weekB.AddDays(4), null, false, null, false, null, null));
|
||||||
|
|
||||||
|
Assert.Equal(1, resultA.ScheduledRows[0].Id);
|
||||||
|
Assert.Equal(2, resultB.ScheduledRows[0].Id);
|
||||||
|
Assert.Empty(resultA.UnscheduledRows);
|
||||||
|
Assert.Empty(resultB.UnscheduledRows);
|
||||||
|
Assert.Equal(1, resultA.ScheduledTotalBeforeSearch);
|
||||||
|
Assert.Equal(1, resultB.ScheduledTotalBeforeSearch);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetBoardRows_FiltersByDispatcher()
|
public async Task GetBoardRows_FiltersByDispatcher()
|
||||||
{
|
{
|
||||||
|
|
@ -118,6 +163,121 @@ public class WorkOrderBoardDataServiceTests
|
||||||
Assert.Equal("user-a", result.ScheduledRows[0].AssignTo);
|
Assert.Equal("user-a", result.ScheduledRows[0].AssignTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardRows_DatedUnassigned_AppearsInScheduledNotUnscheduled()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
|
||||||
|
context.workOrders.AddRange(
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
AssignTo = null,
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 23),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
InternalWONumber = "10000000002",
|
||||||
|
AssignTo = "user-a",
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 24),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
InternalWONumber = "10000000003",
|
||||||
|
AssignTo = null,
|
||||||
|
ScheduledDate = null,
|
||||||
|
LifecycleStatus = LifecycleStatus.Incomplete
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new WorkOrderBoardDataService(context);
|
||||||
|
var result = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||||
|
weekStart,
|
||||||
|
weekStart.AddDays(4),
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null));
|
||||||
|
|
||||||
|
Assert.Equal(2, result.ScheduledRows.Count);
|
||||||
|
Assert.Contains(result.ScheduledRows, r => r.Id == 1 && r.AssignTo == null);
|
||||||
|
Assert.Contains(result.ScheduledRows, r => r.Id == 2 && r.AssignTo == "user-a");
|
||||||
|
Assert.DoesNotContain(result.ScheduledRows, r => r.Id == 3);
|
||||||
|
Assert.Empty(result.UnscheduledRows);
|
||||||
|
Assert.Equal(2, result.ScheduledTotalBeforeSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardRows_FiltersByUnassignedDispatcher_ReturnsOnlyUnassignedInWeek()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
|
||||||
|
context.workOrders.AddRange(
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
AssignTo = null,
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 23),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
InternalWONumber = "10000000002",
|
||||||
|
AssignTo = "user-a",
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 24),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
InternalWONumber = "10000000003",
|
||||||
|
AssignTo = "",
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 25),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 4,
|
||||||
|
InternalWONumber = "10000000004",
|
||||||
|
AssignTo = null,
|
||||||
|
ScheduledDate = null,
|
||||||
|
LifecycleStatus = LifecycleStatus.Incomplete
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new WorkOrderBoardDataService(context);
|
||||||
|
var result = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||||
|
weekStart,
|
||||||
|
weekStart.AddDays(4),
|
||||||
|
new[] { "__unassigned__" },
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null));
|
||||||
|
|
||||||
|
Assert.Equal(2, result.ScheduledRows.Count);
|
||||||
|
Assert.Contains(result.ScheduledRows, r => r.Id == 1);
|
||||||
|
Assert.Contains(result.ScheduledRows, r => r.Id == 3);
|
||||||
|
Assert.DoesNotContain(result.ScheduledRows, r => r.Id == 2);
|
||||||
|
Assert.DoesNotContain(result.ScheduledRows, r => r.Id == 4);
|
||||||
|
Assert.Empty(result.UnscheduledRows);
|
||||||
|
Assert.Equal(2, result.ScheduledTotalBeforeSearch);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetBoardRows_MyWorkOrders_FiltersCurrentUser()
|
public async Task GetBoardRows_MyWorkOrders_FiltersCurrentUser()
|
||||||
{
|
{
|
||||||
|
|
@ -374,6 +534,122 @@ public class WorkOrderBoardPendingUpliftTests
|
||||||
Assert.Single(result.ScheduledRows);
|
Assert.Single(result.ScheduledRows);
|
||||||
Assert.Equal(0, result.ScheduledRows[0].PendingUpliftCount);
|
Assert.Equal(0, result.ScheduledRows[0].PendingUpliftCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardRows_PendingUpliftOnPrimaryAndDispatchWorkOrderLink_DoesNotDoubleCount()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
|
||||||
|
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
|
||||||
|
context.Dispatches.Add(new Dispatch
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
VendorId = 1,
|
||||||
|
WorkOrderId = 1
|
||||||
|
});
|
||||||
|
context.DispatchWorkOrders.Add(new DispatchWorkOrder
|
||||||
|
{
|
||||||
|
DispatchId = 10,
|
||||||
|
WorkOrderId = 1
|
||||||
|
});
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 23),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
PrimaryDispatchId = 10
|
||||||
|
});
|
||||||
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
||||||
|
{
|
||||||
|
Id = 100,
|
||||||
|
DispatchId = 10,
|
||||||
|
RequestedNTE = 1500m,
|
||||||
|
Status = "Pending",
|
||||||
|
RequiredTier = 1,
|
||||||
|
NotificationStatus = "Pending"
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new WorkOrderBoardDataService(context);
|
||||||
|
var result = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||||
|
weekStart,
|
||||||
|
weekStart.AddDays(4),
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null));
|
||||||
|
|
||||||
|
Assert.Single(result.ScheduledRows);
|
||||||
|
Assert.Equal(1, result.ScheduledRows[0].PendingUpliftCount);
|
||||||
|
Assert.True(result.ScheduledRows[0].HasUplift);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardRows_ScheduledDateWithTimeOnWeekEnd_IsIncluded()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
var weekEnd = weekStart.AddDays(4);
|
||||||
|
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
ScheduledDate = weekEnd.ToDateTime(new TimeOnly(23, 59, 59)),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new WorkOrderBoardDataService(context);
|
||||||
|
var result = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||||
|
weekStart,
|
||||||
|
weekEnd,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null));
|
||||||
|
|
||||||
|
Assert.Single(result.ScheduledRows);
|
||||||
|
Assert.Equal(1, result.ScheduledRows[0].Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardRows_CanceledToken_ThrowsOperationCanceled()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 23),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
var service = new WorkOrderBoardDataService(context);
|
||||||
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
||||||
|
service.GetBoardRowsAsync(
|
||||||
|
new WorkOrderBoardQuery(
|
||||||
|
weekStart,
|
||||||
|
weekStart.AddDays(4),
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null),
|
||||||
|
cts.Token));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WorkOrderBoardServiceTests
|
public class WorkOrderBoardServiceTests
|
||||||
|
|
@ -448,6 +724,111 @@ public class WorkOrderBoardServiceTests
|
||||||
Assert.Equal(WorkOrderType.PM, response.Scheduled[0].WorkOrderType);
|
Assert.Equal(WorkOrderType.PM, response.Scheduled[0].WorkOrderType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardAsync_MapsAvetaRequiredAndOriginalDate()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
WorkOrderType = WorkOrderType.PM,
|
||||||
|
AvetaRequired = true,
|
||||||
|
OriginalDate = new DateOnly(2026, 5, 8),
|
||||||
|
OriginalWeek = new DateOnly(2026, 5, 4),
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 24, 12, 0, 0, DateTimeKind.Utc),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var boardService = new WorkOrderBoardService(
|
||||||
|
new WorkOrderBoardDataService(context),
|
||||||
|
WorkOrderAccountTestHelpers.Resolver(context));
|
||||||
|
var response = await boardService.GetBoardAsync(new WorkOrderBoardQueryDto
|
||||||
|
{
|
||||||
|
WeekStart = weekStart
|
||||||
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
||||||
|
|
||||||
|
Assert.Single(response.Scheduled);
|
||||||
|
Assert.True(response.Scheduled[0].AvetaRequired);
|
||||||
|
Assert.Equal(new DateOnly(2026, 5, 8), response.Scheduled[0].OriginalDate);
|
||||||
|
Assert.Equal(new DateOnly(2026, 5, 4), response.Scheduled[0].OriginalWeek);
|
||||||
|
Assert.False(response.Scheduled[0].HasAvetaDocument);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardAsync_HasAvetaDocument_WhenAvetaAttachmentExists()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
AvetaRequired = true,
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 24, 12, 0, 0, DateTimeKind.Utc),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
});
|
||||||
|
context.workOrderAttachments.Add(new WorkOrderAttachments
|
||||||
|
{
|
||||||
|
WorkorderId = 1,
|
||||||
|
Attachments = "https://example.com/aveta.pdf",
|
||||||
|
Category = WorkOrderMediaCategory.Aveta
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var boardService = new WorkOrderBoardService(
|
||||||
|
new WorkOrderBoardDataService(context),
|
||||||
|
WorkOrderAccountTestHelpers.Resolver(context));
|
||||||
|
var response = await boardService.GetBoardAsync(new WorkOrderBoardQueryDto
|
||||||
|
{
|
||||||
|
WeekStart = weekStart
|
||||||
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
||||||
|
|
||||||
|
Assert.Single(response.Scheduled);
|
||||||
|
Assert.True(response.Scheduled[0].AvetaRequired);
|
||||||
|
Assert.True(response.Scheduled[0].HasAvetaDocument);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardAsync_HasAvetaDocumentFalse_WhenOnlyExtraAttachment()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
AvetaRequired = true,
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 24, 12, 0, 0, DateTimeKind.Utc),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
});
|
||||||
|
context.workOrderAttachments.Add(new WorkOrderAttachments
|
||||||
|
{
|
||||||
|
WorkorderId = 1,
|
||||||
|
Attachments = "https://example.com/extra.pdf",
|
||||||
|
Category = WorkOrderMediaCategory.Extra
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var boardService = new WorkOrderBoardService(
|
||||||
|
new WorkOrderBoardDataService(context),
|
||||||
|
WorkOrderAccountTestHelpers.Resolver(context));
|
||||||
|
var response = await boardService.GetBoardAsync(new WorkOrderBoardQueryDto
|
||||||
|
{
|
||||||
|
WeekStart = weekStart
|
||||||
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
||||||
|
|
||||||
|
Assert.Single(response.Scheduled);
|
||||||
|
Assert.True(response.Scheduled[0].AvetaRequired);
|
||||||
|
Assert.False(response.Scheduled[0].HasAvetaDocument);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetBoardAsync_InvalidWeekWindow_Throws()
|
public async Task GetBoardAsync_InvalidWeekWindow_Throws()
|
||||||
{
|
{
|
||||||
|
|
@ -460,4 +841,32 @@ public class WorkOrderBoardServiceTests
|
||||||
WeekEnd = new DateOnly(2026, 6, 15)
|
WeekEnd = new DateOnly(2026, 6, 15)
|
||||||
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null));
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardAsync_CanceledToken_ThrowsOperationCanceled()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 23),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var boardService = new WorkOrderBoardService(
|
||||||
|
new WorkOrderBoardDataService(context),
|
||||||
|
WorkOrderAccountTestHelpers.Resolver(context));
|
||||||
|
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
||||||
|
boardService.GetBoardAsync(
|
||||||
|
new WorkOrderBoardQueryDto { WeekStart = new DateOnly(2026, 6, 22) },
|
||||||
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
||||||
|
null,
|
||||||
|
cts.Token));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1388,4 +1388,80 @@ public class WorkOrderBoardUpdateServiceTests
|
||||||
Assert.True(persisted.IsAddOn);
|
Assert.True(persisted.IsAddOn);
|
||||||
Assert.Empty(await context.WorkOrderAuditLogs.Where(a => a.FieldName == "IsAddOn").ToListAsync());
|
Assert.Empty(await context.WorkOrderAuditLogs.Where(a => a.FieldName == "IsAddOn").ToListAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PatchField_AvetaRequiredTrue_PersistsAndAudits()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
var wo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
LifecycleStatus = LifecycleStatus.Incomplete,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
};
|
||||||
|
context.workOrders.Add(wo);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||||
|
{
|
||||||
|
Field = WorkOrderBoardFieldNames.AvetaRequired,
|
||||||
|
Value = "true",
|
||||||
|
WorkOrderVersion = ToVersion(wo)
|
||||||
|
}, "actor-1");
|
||||||
|
|
||||||
|
Assert.True(result.AvetaRequired);
|
||||||
|
Assert.True((await context.workOrders.FindAsync(1))!.AvetaRequired);
|
||||||
|
Assert.Contains(
|
||||||
|
await context.WorkOrderAuditLogs.ToListAsync(),
|
||||||
|
a => a.FieldName == "AvetaRequired" && a.Action == AuditActionType.FieldChanged.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PatchField_AvetaRequiredFalse_ClearsFlag()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
var wo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
LifecycleStatus = LifecycleStatus.Incomplete,
|
||||||
|
AvetaRequired = true,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
};
|
||||||
|
context.workOrders.Add(wo);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||||
|
{
|
||||||
|
Field = WorkOrderBoardFieldNames.AvetaRequired,
|
||||||
|
Value = "false",
|
||||||
|
WorkOrderVersion = ToVersion(wo)
|
||||||
|
}, "actor-1");
|
||||||
|
|
||||||
|
Assert.False(result.AvetaRequired);
|
||||||
|
Assert.False((await context.workOrders.FindAsync(1))!.AvetaRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PatchField_AvetaRequiredInvalid_ThrowsInvalidValue()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
var wo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
LifecycleStatus = LifecycleStatus.Incomplete,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
};
|
||||||
|
context.workOrders.Add(wo);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
||||||
|
service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||||
|
{
|
||||||
|
Field = WorkOrderBoardFieldNames.AvetaRequired,
|
||||||
|
Value = "yes",
|
||||||
|
WorkOrderVersion = ToVersion(wo)
|
||||||
|
}, "actor-1"));
|
||||||
|
|
||||||
|
Assert.Equal("InvalidValue", ex.Code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,24 @@ public class WorkOrderCompletedSelectiveLockTests
|
||||||
Assert.Equal(WorkOrderMediaCategory.Extra, media.Category);
|
Assert.Equal(WorkOrderMediaCategory.Extra, media.Category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddMedia_Completed_AllowsAveta()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateMediaSut();
|
||||||
|
context.workOrders.Add(CompletedWorkOrder());
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var media = await service.AddMediaAsync(
|
||||||
|
1,
|
||||||
|
WorkOrderMediaCategory.Aveta,
|
||||||
|
"https://example.com/aveta.pdf",
|
||||||
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
||||||
|
"actor-1");
|
||||||
|
|
||||||
|
Assert.True(media.Id > 0);
|
||||||
|
Assert.Equal(WorkOrderMediaCategory.Aveta, media.Category);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddMedia_Completed_BlocksBefore()
|
public async Task AddMedia_Completed_BlocksBefore()
|
||||||
{
|
{
|
||||||
|
|
@ -286,6 +304,32 @@ public class WorkOrderCompletedSelectiveLockTests
|
||||||
Assert.Equal(WorkOrderMediaCategory.Extra, media.Category);
|
Assert.Equal(WorkOrderMediaCategory.Extra, media.Category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateMediaCategory_Completed_AllowsAveta()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateMediaSut();
|
||||||
|
var wo = CompletedWorkOrder();
|
||||||
|
context.workOrders.Add(wo);
|
||||||
|
context.workOrderAttachments.Add(new WorkOrderAttachments
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
WorkorderId = 1,
|
||||||
|
Attachments = "https://example.com/extra.pdf",
|
||||||
|
Category = WorkOrderMediaCategory.Extra
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var media = await service.UpdateMediaCategoryAsync(
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
WorkOrderMediaCategory.Aveta,
|
||||||
|
ToVersion(wo),
|
||||||
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
||||||
|
"actor-1");
|
||||||
|
|
||||||
|
Assert.Equal(WorkOrderMediaCategory.Aveta, media.Category);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UpdateMediaCategory_Completed_BlocksMoveToBefore()
|
public async Task UpdateMediaCategory_Completed_BlocksMoveToBefore()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,17 @@ public class WorkOrderMigrationDiscoveryTests
|
||||||
|
|
||||||
Assert.Contains("20260713120000_Phase7_ExternalWorkOrderIdUnique", migrations);
|
Assert.Contains("20260713120000_Phase7_ExternalWorkOrderIdUnique", migrations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SH117_AvetaRequired_is_discoverable_by_ef_runtime()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||||
|
.UseSqlite("DataSource=:memory:")
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
using var context = new ApplicationDbContext(options);
|
||||||
|
var migrations = context.Database.GetMigrations().ToList();
|
||||||
|
|
||||||
|
Assert.Contains("20260824150000_SH117_AvetaRequired", migrations);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -866,6 +866,101 @@ public class WorkOrderMediaServiceTests
|
||||||
Assert.Contains(media, m => m.Category == WorkOrderMediaCategory.Extra);
|
Assert.Contains(media, m => m.Category == WorkOrderMediaCategory.Extra);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetMedia_IncludesAvetaCategory()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
context.workOrders.Add(new WorkOrder { Id = 1 });
|
||||||
|
context.workOrderAttachments.Add(new WorkOrderAttachments
|
||||||
|
{
|
||||||
|
WorkorderId = 1,
|
||||||
|
Attachments = "https://example.com/aveta.pdf",
|
||||||
|
Category = WorkOrderMediaCategory.Aveta
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var media = await service.GetMediaAsync(1, AuthenticatedUser(), "actor-1");
|
||||||
|
|
||||||
|
Assert.NotNull(media);
|
||||||
|
Assert.Contains(media!, m => m.Category == WorkOrderMediaCategory.Aveta);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddMedia_Aveta_PersistsCategory()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var media = await service.AddMediaAsync(
|
||||||
|
1,
|
||||||
|
WorkOrderMediaCategory.Aveta,
|
||||||
|
"https://example.com/aveta.pdf",
|
||||||
|
AuthenticatedUser(),
|
||||||
|
"actor-1");
|
||||||
|
|
||||||
|
Assert.True(media.Id > 0);
|
||||||
|
Assert.Equal(WorkOrderMediaCategory.Aveta, media.Category);
|
||||||
|
Assert.Equal(WorkOrderMediaCategory.Aveta, context.workOrderAttachments.Single().Category);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddMedia_Extra_DoesNotPersistAsAveta()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var media = await service.AddMediaAsync(
|
||||||
|
1,
|
||||||
|
WorkOrderMediaCategory.Extra,
|
||||||
|
"https://example.com/extra.pdf",
|
||||||
|
AuthenticatedUser(),
|
||||||
|
"actor-1");
|
||||||
|
|
||||||
|
Assert.Equal(WorkOrderMediaCategory.Extra, media.Category);
|
||||||
|
Assert.Equal(WorkOrderMediaCategory.Extra, context.workOrderAttachments.Single().Category);
|
||||||
|
Assert.NotEqual(WorkOrderMediaCategory.Aveta, context.workOrderAttachments.Single().Category);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateMediaCategory_ExtraToAveta()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
var wo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
};
|
||||||
|
context.workOrders.Add(wo);
|
||||||
|
context.workOrderAttachments.Add(new WorkOrderAttachments
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
WorkorderId = 1,
|
||||||
|
Attachments = "https://example.com/extra.pdf",
|
||||||
|
Category = WorkOrderMediaCategory.Extra
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var media = await service.UpdateMediaCategoryAsync(
|
||||||
|
1, 10, WorkOrderMediaCategory.Aveta, ToVersion(wo), AuthenticatedUser(), "actor-1");
|
||||||
|
|
||||||
|
Assert.Equal(WorkOrderMediaCategory.Aveta, media.Category);
|
||||||
|
Assert.Equal(WorkOrderMediaCategory.Aveta, context.workOrderAttachments.Single().Category);
|
||||||
|
Assert.True(context.workOrderAttachments.Single().IsDeleted != true);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetMedia_TechnicianOnAssignedWorkOrder_Succeeds()
|
public async Task GetMedia_TechnicianOnAssignedWorkOrder_Succeeds()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue