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/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs index 0ccb890..015c2ad 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,9 @@ namespace SeaHaven.DataServices.Helpers 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, @@ -180,6 +184,7 @@ namespace SeaHaven.DataServices.Helpers w.PrimaryUpliftAmount, w.PrimaryDispatchStatus, w.AvetaRequired, + w.HasAvetaDocument, w.OriginalDate, w.OriginalWeek, isUnscheduled); diff --git a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs index d4cb09c..da84f94 100644 --- a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs +++ b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs @@ -60,6 +60,7 @@ namespace SeaHaven.DataServices.Interfaces decimal? PrimaryUpliftAmount, string? PrimaryDispatchStatus, bool AvetaRequired, + bool HasAvetaDocument, DateOnly? OriginalDate, DateOnly? OriginalWeek, bool IsUnscheduled); diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs index 395e858..5e960d3 100644 --- a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs @@ -37,6 +37,7 @@ namespace SeaHaven.Services.DTOs 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; } 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/WorkOrderBoardService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs index c4405b3..335e71c 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs @@ -100,6 +100,7 @@ namespace SeaHaven.Services.Implementation CarriedOver = row.CarriedOver, IsAddOn = row.IsAddOn, AvetaRequired = row.AvetaRequired, + HasAvetaDocument = row.HasAvetaDocument, OriginalDate = row.OriginalDate, OriginalWeek = row.OriginalWeek, WorkOrderType = row.WorkOrderType, diff --git a/SeaHaven.Services/Implementation/WorkOrderDetailService.cs b/SeaHaven.Services/Implementation/WorkOrderDetailService.cs index b30f052..7f4d132 100644 --- a/SeaHaven.Services/Implementation/WorkOrderDetailService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderDetailService.cs @@ -97,6 +97,7 @@ namespace SeaHaven.Services.Implementation CarriedOver = row.CarriedOver, IsAddOn = row.IsAddOn, AvetaRequired = row.AvetaRequired, + HasAvetaDocument = row.HasAvetaDocument, WorkOrderType = row.WorkOrderType, IsPastDue = row.IsPastDue, SiteCode = row.SiteCode, 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/WorkOrderBoardServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs index a13bc22..a73d582 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs @@ -480,6 +480,77 @@ public class WorkOrderBoardServiceTests 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] 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/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() {