diff --git a/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs b/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs index b8db7fc..f50824f 100644 --- a/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs +++ b/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs @@ -165,7 +165,8 @@ namespace Data.SeaHavenIndustries.Enums Before = 1, After = 2, Extra = 3, - Completion = 4 + Completion = 4, + Aveta = 5 } } \ No newline at end of file diff --git a/Data.SeaHavenIndustries/Migrations/20260824150000_SH117_AvetaRequired.cs b/Data.SeaHavenIndustries/Migrations/20260824150000_SH117_AvetaRequired.cs new file mode 100644 index 0000000..eb71969 --- /dev/null +++ b/Data.SeaHavenIndustries/Migrations/20260824150000_SH117_AvetaRequired.cs @@ -0,0 +1,33 @@ +using Data.SeaHavenIndustries; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.SeaHavenIndustries.Migrations +{ + /// + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260824150000_SH117_AvetaRequired")] + public partial class SH117_AvetaRequired : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AvetaRequired", + table: "workOrders", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AvetaRequired", + table: "workOrders"); + } + } +} diff --git a/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs b/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs index 0efc712..a3051ed 100644 --- a/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs @@ -2410,6 +2410,9 @@ namespace Data.SeaHavenIndustries.Migrations b.Property("Attachments") .HasColumnType("nvarchar(max)"); + b.Property("AvetaRequired") + .HasColumnType("bit"); + b.Property("AvettaTask") .HasColumnType("nvarchar(max)"); diff --git a/Data.SeaHavenIndustries/Models/WorkerOrder.cs b/Data.SeaHavenIndustries/Models/WorkerOrder.cs index 733b01e..5e87a2e 100644 --- a/Data.SeaHavenIndustries/Models/WorkerOrder.cs +++ b/Data.SeaHavenIndustries/Models/WorkerOrder.cs @@ -62,6 +62,10 @@ namespace Data.SeaHavenIndustries public string? ExtraServices { get; set; } [MaxLength(4000)] public string? ServiceNotes { get; set; } + /// + /// Per-WO Aveta compliance flag. Distinct from legacy AvettaTask string. + /// + public bool AvetaRequired { get; set; } /// Tech/crew phone when no primary dispatch; otherwise prefer Dispatch.TechPhone. [MaxLength(50)] public string? TechPhone { get; set; } diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs index 9e592e6..aaf1778 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs @@ -1,4 +1,5 @@ using Data.SeaHavenIndustries; +using Data.SeaHavenIndustries.Enums; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Interfaces; @@ -20,6 +21,12 @@ namespace SeaHaven.DataServices.Helpers w.RescheduleCount, w.CarriedOver, 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.SiteCode, LocationName = w.Locations != null ? w.Locations.Name : null, @@ -144,6 +151,10 @@ namespace SeaHaven.DataServices.Helpers uplift?.PrimaryStatus, uplift?.PrimaryAmount, w.PrimaryDispatchStatus, + w.AvetaRequired, + w.HasAvetaDocument, + w.OriginalDate, + w.OriginalWeek, isUnscheduled); }).ToList(); } diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFacetFilters.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFacetFilters.cs index 08b2c2d..d3bdfea 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFacetFilters.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFacetFilters.cs @@ -74,6 +74,16 @@ namespace SeaHaven.DataServices.Helpers return query.Where(w => w.IsAddOn); } + public static IQueryable ApplyAvetaOnlyFilter( + IQueryable query, + bool avetaOnly) + { + if (!avetaOnly) + return query; + + return query.Where(w => w.AvetaRequired); + } + public static IQueryable ApplyFlagColorFilter( IQueryable query, IReadOnlyList? flagColors) diff --git a/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs b/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs index 1d4892d..2eebc30 100644 --- a/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs +++ b/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs @@ -32,6 +32,7 @@ namespace SeaHaven.DataServices.Implementation baseQuery = WorkOrderBoardSearchFacetFilters.ApplyRescheduledFilter(baseQuery, query.Rescheduled); baseQuery = WorkOrderBoardSearchFacetFilters.ApplyCarriedOverFilter(baseQuery, query.CarriedOver); baseQuery = WorkOrderBoardSearchFacetFilters.ApplyAddOnFilter(baseQuery, query.AddOn); + baseQuery = WorkOrderBoardSearchFacetFilters.ApplyAvetaOnlyFilter(baseQuery, query.AvetaOnly); baseQuery = WorkOrderBoardSearchFacetFilters.ApplyFlagColorFilter(baseQuery, query.FlagColors); baseQuery = WorkOrderBoardSearchFacetFilters.ApplyInternalOnlyFilter(baseQuery, query.InternalOnly); baseQuery = WorkOrderBoardSearchFacetFilters.ApplyUpliftFilter( diff --git a/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs b/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs index b965e86..44889dc 100644 --- a/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs +++ b/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs @@ -19,6 +19,7 @@ namespace SeaHaven.DataServices.Interfaces bool Rescheduled, bool CarriedOver, bool AddOn, + bool AvetaOnly, IReadOnlyList? FlagColors, bool InternalOnly, bool HasUplift, diff --git a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs index 6ae2928..da84f94 100644 --- a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs +++ b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs @@ -59,6 +59,10 @@ namespace SeaHaven.DataServices.Interfaces string? PrimaryUpliftStatus, decimal? PrimaryUpliftAmount, string? PrimaryDispatchStatus, + bool AvetaRequired, + bool HasAvetaDocument, + DateOnly? OriginalDate, + DateOnly? OriginalWeek, bool IsUnscheduled); public record WorkOrderBoardQueryResult( diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs index d26295c..c4704ea 100644 --- a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs @@ -41,6 +41,10 @@ namespace SeaHaven.Services.DTOs public int RescheduleCount { get; set; } public int CarriedOver { 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 bool IsPastDue { get; set; } public string? SiteCode { get; set; } diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs index 9a5de5d..d9718ff 100644 --- a/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs @@ -32,6 +32,8 @@ namespace SeaHaven.Services.DTOs public string? PrimaryService { get; set; } public List? ExtraServices { get; set; } public string? ServiceNotes { get; set; } + /// Per-WO Aveta compliance. Omit or false = not required. + public bool? AvetaRequired { get; set; } public int? LocationId { get; set; } public DateTime? DueDate { get; set; } public string? AssignTo { get; set; } @@ -73,6 +75,8 @@ namespace SeaHaven.Services.DTOs public bool Rescheduled { get; set; } public bool CarriedOver { get; set; } public bool AddOn { get; set; } + /// When true, only rows with AvetaRequired. + public bool AvetaOnly { get; set; } public List? FlagColors { get; set; } public bool InternalOnly { get; set; } public bool HasUplift { get; set; } diff --git a/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs index 8f2c692..9832b01 100644 --- a/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs @@ -17,8 +17,6 @@ namespace SeaHaven.Services.DTOs public string? Trade { get; set; } public string? Problem { get; set; } public string? SubTrade { get; set; } - public DateOnly? OriginalWeek { get; set; } - public DateOnly? OriginalDate { get; set; } public string? VendorNotes { get; set; } } diff --git a/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs b/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs index 45ae58f..4056197 100644 --- a/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs +++ b/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs @@ -21,6 +21,7 @@ namespace SeaHaven.Services.Helpers public const string ExtraServices = "extraServices"; public const string AdditionalContacts = "additionalContacts"; public const string FlagColor = "flagColor"; + public const string AvetaRequired = "avetaRequired"; public const string RescheduleCount = "RescheduleCount"; private static readonly HashSet DispatchFields = new(StringComparer.OrdinalIgnoreCase) @@ -50,7 +51,8 @@ namespace SeaHaven.Services.Helpers ServiceNotes, ExtraServices, AdditionalContacts, - FlagColor + FlagColor, + AvetaRequired }; public static bool IsEditable(string field) => Canonicalize(field) != null; @@ -93,6 +95,7 @@ namespace SeaHaven.Services.Helpers ExtraServices => "ExtraServices", AdditionalContacts => "AdditionalContacts", FlagColor => "FlagColor", + AvetaRequired => "AvetaRequired", _ => canonical }; } diff --git a/SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs b/SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs index 7a52474..dde4bfb 100644 --- a/SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs +++ b/SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs @@ -33,7 +33,13 @@ namespace SeaHaven.Services.Helpers return false; if (!IsCoreLocked(status)) 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) diff --git a/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs b/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs index 888a718..071acf0 100644 --- a/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs @@ -55,6 +55,7 @@ namespace SeaHaven.Services.Implementation query.Rescheduled, query.CarriedOver, query.AddOn, + query.AvetaOnly, query.FlagColors, query.InternalOnly, query.HasUplift, diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs index a5df677..f644f6e 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs @@ -85,6 +85,7 @@ namespace SeaHaven.Services.Implementation Trade = primaryService, ExtraServices = extraServicesJson, ServiceNotes = serviceNotes, + AvetaRequired = request.AvetaRequired == true, PocName = pocName, PocPhone = pocPhone, PocNotes = pocNotes, @@ -130,6 +131,8 @@ namespace SeaHaven.Services.Implementation changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ExtraServices", "", workOrder.ExtraServices)); if (workOrder.ServiceNotes != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ServiceNotes", "", workOrder.ServiceNotes)); + if (workOrder.AvetaRequired) + changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("AvetaRequired", "", bool.TrueString)); if (workOrder.PocName != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("PocName", "", workOrder.PocName)); if (workOrder.PocPhone != null) diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs index 1f03160..972e2f1 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs @@ -100,6 +100,10 @@ namespace SeaHaven.Services.Implementation RescheduleCount = row.RescheduleCount, CarriedOver = row.CarriedOver, IsAddOn = row.IsAddOn, + AvetaRequired = row.AvetaRequired, + HasAvetaDocument = row.HasAvetaDocument, + OriginalDate = row.OriginalDate, + OriginalWeek = row.OriginalWeek, WorkOrderType = row.WorkOrderType, IsPastDue = WorkOrderDerivedFields.IsPastDue(row.ScheduledDate, row.LifecycleStatus, utcNow), SiteCode = row.SiteCode, diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs index 5a11b21..6c27693 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs @@ -193,6 +193,7 @@ namespace SeaHaven.Services.Implementation WorkOrderBoardFieldNames.ExtraServices => new List { ApplyExtraServices(workOrder, value, auditField) }, WorkOrderBoardFieldNames.AdditionalContacts => new List { ApplyAdditionalContacts(workOrder, value, auditField) }, WorkOrderBoardFieldNames.FlagColor => new List { ApplyFlagColor(workOrder, value, auditField) }, + WorkOrderBoardFieldNames.AvetaRequired => new List { ApplyAvetaRequired(workOrder, value, auditField) }, _ => throw new WorkOrderBoardValidationException("InvalidField", $"Field '{field}' is not editable.") }; } @@ -679,6 +680,15 @@ namespace SeaHaven.Services.Implementation 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) { if (!WorkOrderFlagColors.TryNormalize(value, out var canonical)) diff --git a/SeaHaven.Services/Implementation/WorkOrderDetailService.cs b/SeaHaven.Services/Implementation/WorkOrderDetailService.cs index 9fcf0ab..7f4d132 100644 --- a/SeaHaven.Services/Implementation/WorkOrderDetailService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderDetailService.cs @@ -96,6 +96,8 @@ namespace SeaHaven.Services.Implementation RescheduleCount = row.RescheduleCount, CarriedOver = row.CarriedOver, IsAddOn = row.IsAddOn, + AvetaRequired = row.AvetaRequired, + HasAvetaDocument = row.HasAvetaDocument, WorkOrderType = row.WorkOrderType, IsPastDue = row.IsPastDue, SiteCode = row.SiteCode, @@ -137,8 +139,8 @@ namespace SeaHaven.Services.Implementation Trade = extended?.Trade, Problem = extended?.Problem, SubTrade = extended?.SubTrade, - OriginalWeek = extended?.OriginalWeek, - OriginalDate = extended?.OriginalDate, + OriginalWeek = row.OriginalWeek ?? extended?.OriginalWeek, + OriginalDate = row.OriginalDate ?? extended?.OriginalDate, CompletedDate = extended?.CompletedDate, VendorNotes = extended?.VendorNotes }; diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs index d3df3a9..53f4f84 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs @@ -432,6 +432,45 @@ public class WorkOrderBoardCreateServiceTests 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] public async Task Create_WorkOrderTypeAddOn_ThrowsValidation() { diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardMutationRulesTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardMutationRulesTests.cs index 195c8d1..6494004 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardMutationRulesTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardMutationRulesTests.cs @@ -87,8 +87,12 @@ public class WorkOrderBoardMutationRulesTests LifecycleStatus.Completed, WorkOrderMediaCategory.Before)); Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia( LifecycleStatus.Completed, WorkOrderMediaCategory.After)); + Assert.True(WorkOrderBoardMutationRules.CanMutateExtraMedia( + LifecycleStatus.Completed, WorkOrderMediaCategory.Aveta)); Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia( LifecycleStatus.Canceled, WorkOrderMediaCategory.Extra)); + Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia( + LifecycleStatus.Canceled, WorkOrderMediaCategory.Aveta)); } [Fact] diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs index fe3d37d..9c8dd75 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs @@ -1001,4 +1001,51 @@ public class WorkOrderAdvancedSearchServiceTests Assert.Equal(1, result.TotalCount); 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); + } } diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs index b9fe3a4..123cc03 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs @@ -724,6 +724,111 @@ public class WorkOrderBoardServiceTests 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] public async Task GetBoardAsync_InvalidWeekWindow_Throws() { diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs index ebf9996..9bdf934 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs @@ -1388,4 +1388,80 @@ public class WorkOrderBoardUpdateServiceTests Assert.True(persisted.IsAddOn); 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(() => + service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto + { + Field = WorkOrderBoardFieldNames.AvetaRequired, + Value = "yes", + WorkOrderVersion = ToVersion(wo) + }, "actor-1")); + + Assert.Equal("InvalidValue", ex.Code); + } } diff --git a/SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs b/SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs index 9ebd221..733c589 100644 --- a/SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs @@ -242,6 +242,24 @@ public class WorkOrderCompletedSelectiveLockTests 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] public async Task AddMedia_Completed_BlocksBefore() { @@ -286,6 +304,32 @@ public class WorkOrderCompletedSelectiveLockTests 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] public async Task UpdateMediaCategory_Completed_BlocksMoveToBefore() { diff --git a/SeaHavenIndustries.Tests/WorkOrderMigrationDiscoveryTests.cs b/SeaHavenIndustries.Tests/WorkOrderMigrationDiscoveryTests.cs index 5484926..8e34d94 100644 --- a/SeaHavenIndustries.Tests/WorkOrderMigrationDiscoveryTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderMigrationDiscoveryTests.cs @@ -19,4 +19,17 @@ public class WorkOrderMigrationDiscoveryTests Assert.Contains("20260713120000_Phase7_ExternalWorkOrderIdUnique", migrations); } + + [Fact] + public void SH117_AvetaRequired_is_discoverable_by_ef_runtime() + { + var options = new DbContextOptionsBuilder() + .UseSqlite("DataSource=:memory:") + .Options; + + using var context = new ApplicationDbContext(options); + var migrations = context.Database.GetMigrations().ToList(); + + Assert.Contains("20260824150000_SH117_AvetaRequired", migrations); + } } diff --git a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs index 1586a83..7e6a254 100644 --- a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs @@ -866,6 +866,101 @@ public class WorkOrderMediaServiceTests 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] public async Task GetMedia_TechnicianOnAssignedWorkOrder_Succeeds() {