mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
Merge branch 'dev' into feat/ab/sh-348-dashboard-region
This commit is contained in:
commit
7e06d1c51b
15 changed files with 4435 additions and 5 deletions
|
|
@ -80,6 +80,12 @@ namespace Data.SeaHavenIndustries
|
|||
.HasIndex(w => w.InternalWONumber)
|
||||
.HasFilter("[istemplate] = 0");
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.HasOne(w => w.ServiceDefinition)
|
||||
.WithMany()
|
||||
.HasForeignKey(w => w.ServiceId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.HasIndex(w => new { w.LifecycleStatus, w.ScheduledDate })
|
||||
.HasFilter("[istemplate] = 0");
|
||||
|
|
|
|||
4134
Data.SeaHavenIndustries/Migrations/20260916204606_SH187_WorkOrderServiceLink.Designer.cs
generated
Normal file
4134
Data.SeaHavenIndustries/Migrations/20260916204606_SH187_WorkOrderServiceLink.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,73 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SH187_WorkOrderServiceLink : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ServiceId",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ServiceNameSnapshot",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(200)",
|
||||
maxLength: 200,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE w
|
||||
SET
|
||||
w.[ServiceNameSnapshot] = LEFT(COALESCE(NULLIF(LTRIM(RTRIM(w.[Trade])), N''), NULLIF(LTRIM(RTRIM(w.[Problem])), N'')), 200),
|
||||
w.[ServiceId] = s.[Id]
|
||||
FROM [workOrders] w
|
||||
LEFT JOIN [Services] s
|
||||
ON (s.[IsDeleted] <> 1 OR s.[IsDeleted] IS NULL)
|
||||
AND s.[NormalizedName] = LOWER(LTRIM(RTRIM(COALESCE(NULLIF(LTRIM(RTRIM(w.[Trade])), N''), NULLIF(LTRIM(RTRIM(w.[Problem])), N'')))))
|
||||
WHERE w.[ServiceNameSnapshot] IS NULL;
|
||||
""");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_workOrders_ServiceId",
|
||||
table: "workOrders",
|
||||
column: "ServiceId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_workOrders_Services_ServiceId",
|
||||
table: "workOrders",
|
||||
column: "ServiceId",
|
||||
principalTable: "Services",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_workOrders_Services_ServiceId",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_workOrders_ServiceId",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ServiceId",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ServiceNameSnapshot",
|
||||
table: "workOrders");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2763,6 +2763,13 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<int?>("ServiceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ServiceNameSnapshot")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("ServiceNotes")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("nvarchar(4000)");
|
||||
|
|
@ -2847,6 +2854,8 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
b.HasIndex("ScheduledDate");
|
||||
|
||||
b.HasIndex("ServiceId");
|
||||
|
||||
b.HasIndex("SiteCode")
|
||||
.HasFilter("[istemplate] = 0");
|
||||
|
||||
|
|
@ -3876,6 +3885,11 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
.HasForeignKey("PrimaryDispatchId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("Data.SeaHavenIndustries.Service", "ServiceDefinition")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServiceId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("AssignToUser");
|
||||
|
|
@ -3883,6 +3897,8 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Navigation("Locations");
|
||||
|
||||
b.Navigation("PrimaryDispatch");
|
||||
|
||||
b.Navigation("ServiceDefinition");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderAttachments", b =>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ namespace Data.SeaHavenIndustries
|
|||
/// <summary>SHOC board "Service" column (PM type label). Not Trade/Problem.</summary>
|
||||
[MaxLength(128)]
|
||||
public string? Service { get; set; }
|
||||
public int? ServiceId { get; set; }
|
||||
[MaxLength(200)]
|
||||
public string? ServiceNameSnapshot { get; set; }
|
||||
[ForeignKey(nameof(ServiceId))]
|
||||
public virtual Service? ServiceDefinition { get; set; }
|
||||
public int? PrimaryDispatchId { get; set; }
|
||||
[ForeignKey(nameof(PrimaryDispatchId))]
|
||||
public virtual Dispatch? PrimaryDispatch { get; set; }
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ namespace SeaHaven.DataServices.Helpers
|
|||
w.OriginalDate,
|
||||
w.OriginalWeek,
|
||||
w.WorkOrderType,
|
||||
w.ServiceId,
|
||||
w.ServiceNameSnapshot,
|
||||
w.SiteCode,
|
||||
LocationName = w.Locations != null ? w.Locations.Name : null,
|
||||
WoPocName = w.PocName,
|
||||
|
|
@ -136,6 +138,8 @@ namespace SeaHaven.DataServices.Helpers
|
|||
w.CarriedOver,
|
||||
w.IsAddOn,
|
||||
w.WorkOrderType,
|
||||
w.ServiceId,
|
||||
w.ServiceNameSnapshot,
|
||||
frozenSite?.Code ?? w.SiteCode,
|
||||
frozenSite?.Label ?? w.LocationName,
|
||||
pocName,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
int CarriedOver,
|
||||
bool IsAddOn,
|
||||
WorkOrderType? WorkOrderType,
|
||||
int? ServiceId,
|
||||
string? ServiceNameSnapshot,
|
||||
string? SiteCode,
|
||||
string? LocationName,
|
||||
string? PocName,
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ namespace SeaHaven.Services.DTOs
|
|||
public DateOnly? OriginalDate { get; set; }
|
||||
public DateOnly? OriginalWeek { get; set; }
|
||||
public WorkOrderType? WorkOrderType { get; set; }
|
||||
public int? ServiceId { get; set; }
|
||||
public string? ServiceNameSnapshot { get; set; }
|
||||
public bool IsPastDue { get; set; }
|
||||
public string? SiteCode { get; set; }
|
||||
public string? LocationName { get; set; }
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ namespace SeaHaven.Services.DTOs
|
|||
public string? Trade { get; set; }
|
||||
/// <summary>Primary service label; persisted as Trade / board pm.</summary>
|
||||
public string? PrimaryService { get; set; }
|
||||
public int? ServiceId { get; set; }
|
||||
public List<string>? ExtraServices { get; set; }
|
||||
public string? ServiceNotes { get; set; }
|
||||
/// <summary>Per-WO Aveta compliance. Omit or false = not required.</summary>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ namespace SeaHaven.Services.Helpers
|
|||
public const string DocStatus = "docStatus";
|
||||
public const string CompletedDate = "completedDate";
|
||||
public const string Pm = "pm";
|
||||
public const string ServiceId = "serviceId";
|
||||
public const string ServiceNotes = "serviceNotes";
|
||||
public const string ExtraServices = "extraServices";
|
||||
public const string AdditionalContacts = "additionalContacts";
|
||||
|
|
@ -49,6 +50,7 @@ namespace SeaHaven.Services.Helpers
|
|||
DocStatus,
|
||||
CompletedDate,
|
||||
Pm,
|
||||
ServiceId,
|
||||
ServiceNotes,
|
||||
ExtraServices,
|
||||
AdditionalContacts,
|
||||
|
|
@ -93,6 +95,7 @@ namespace SeaHaven.Services.Helpers
|
|||
DocStatus => "DocStatus",
|
||||
CompletedDate => "CompletedDate",
|
||||
Pm => "Trade",
|
||||
ServiceId => "ServiceId",
|
||||
ServiceNotes => "ServiceNotes",
|
||||
ExtraServices => "ExtraServices",
|
||||
AdditionalContacts => "AdditionalContacts",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ namespace SeaHaven.Services.Implementation
|
|||
private readonly IWorkOrderAuditService _auditService;
|
||||
private readonly IWorkOrderBoardCreateValidation _validator;
|
||||
private readonly IWorkOrderAccountResolver _accountResolver;
|
||||
private readonly IServicesRegistryService? _servicesRegistryService;
|
||||
|
||||
public WorkOrderBoardCreateService(
|
||||
IWorkOrderBoardDataService boardDataService,
|
||||
|
|
@ -27,7 +28,8 @@ namespace SeaHaven.Services.Implementation
|
|||
IWorkOrderBoardService boardService,
|
||||
IWorkOrderAuditService auditService,
|
||||
IWorkOrderBoardCreateValidation validator,
|
||||
IWorkOrderAccountResolver accountResolver)
|
||||
IWorkOrderAccountResolver accountResolver,
|
||||
IServicesRegistryService? servicesRegistryService = null)
|
||||
{
|
||||
_boardDataService = boardDataService;
|
||||
_mutationData = mutationData;
|
||||
|
|
@ -35,6 +37,7 @@ namespace SeaHaven.Services.Implementation
|
|||
_auditService = auditService;
|
||||
_validator = validator;
|
||||
_accountResolver = accountResolver;
|
||||
_servicesRegistryService = servicesRegistryService;
|
||||
}
|
||||
|
||||
public async Task<WorkOrderBoardRowDto> CreateAsync(
|
||||
|
|
@ -54,7 +57,8 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
var woNumber = await ResolveWoNumberAsync(request.WoNumber, cancellationToken);
|
||||
var siteCode = request.SiteCode!.Trim();
|
||||
var primaryService = ResolvePrimaryService(request);
|
||||
var selectedService = await ResolveServiceAsync(request.ServiceId, request.WorkOrderType, request.PrimaryService ?? request.Trade, cancellationToken);
|
||||
var primaryService = selectedService?.Name ?? ResolvePrimaryService(request);
|
||||
var extraServicesJson = SerializeExtraServices(request.ExtraServices);
|
||||
var serviceNotes = TrimOrNull(request.ServiceNotes);
|
||||
var pocName = TrimOrNull(request.PocName);
|
||||
|
|
@ -87,6 +91,8 @@ namespace SeaHaven.Services.Implementation
|
|||
AccountId = accountId,
|
||||
Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(),
|
||||
Trade = primaryService,
|
||||
ServiceId = selectedService?.Id,
|
||||
ServiceNameSnapshot = selectedService?.Name,
|
||||
ExtraServices = extraServicesJson,
|
||||
ServiceNotes = serviceNotes,
|
||||
AvetaRequired = request.AvetaRequired == true,
|
||||
|
|
@ -138,6 +144,8 @@ namespace SeaHaven.Services.Implementation
|
|||
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("Description", "", workOrder.Description));
|
||||
if (workOrder.Trade != null)
|
||||
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("Trade", "", workOrder.Trade));
|
||||
if (workOrder.ServiceId.HasValue)
|
||||
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ServiceId", "", workOrder.ServiceId.Value.ToString()));
|
||||
if (workOrder.ExtraServices != null)
|
||||
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ExtraServices", "", workOrder.ExtraServices));
|
||||
if (workOrder.ServiceNotes != null)
|
||||
|
|
@ -251,6 +259,46 @@ namespace SeaHaven.Services.Implementation
|
|||
return null;
|
||||
}
|
||||
|
||||
private async Task<ServiceDto?> ResolveServiceAsync(
|
||||
int? serviceId,
|
||||
WorkOrderType workOrderType,
|
||||
string? legacyLabel,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_servicesRegistryService == null)
|
||||
{
|
||||
if (serviceId.HasValue)
|
||||
throw new WorkOrderBoardValidationException("ServiceInvalid", "serviceId is not available.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!ServicesRegistryService.AllowedWorkOrderTypes.Contains(workOrderType))
|
||||
{
|
||||
if (serviceId.HasValue)
|
||||
throw new WorkOrderBoardValidationException(
|
||||
"ServiceInvalid",
|
||||
"serviceId is not supported for this work order type.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var services = await _servicesRegistryService.GetAllAsync(true, workOrderType, cancellationToken);
|
||||
if (serviceId.HasValue)
|
||||
{
|
||||
var selected = services.FirstOrDefault(service => service.Id == serviceId.Value);
|
||||
if (selected == null)
|
||||
throw new WorkOrderBoardValidationException(
|
||||
"ServiceInvalid",
|
||||
"serviceId must reference an active service that supports this work order type.");
|
||||
return selected;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(legacyLabel))
|
||||
return null;
|
||||
|
||||
return services.FirstOrDefault(service =>
|
||||
string.Equals(service.Name.Trim(), legacyLabel.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static string? SerializeExtraServices(List<string>? extras)
|
||||
{
|
||||
if (extras == null || extras.Count == 0)
|
||||
|
|
|
|||
|
|
@ -108,6 +108,8 @@ namespace SeaHaven.Services.Implementation
|
|||
OriginalDate = row.OriginalDate,
|
||||
OriginalWeek = row.OriginalWeek,
|
||||
WorkOrderType = row.WorkOrderType,
|
||||
ServiceId = row.ServiceId,
|
||||
ServiceNameSnapshot = row.ServiceNameSnapshot,
|
||||
IsPastDue = WorkOrderDerivedFields.IsPastDue(row.ScheduledDate, row.LifecycleStatus, utcNow),
|
||||
SiteCode = row.SiteCode,
|
||||
LocationName = row.LocationName,
|
||||
|
|
@ -136,7 +138,7 @@ namespace SeaHaven.Services.Implementation
|
|||
TechPhone = row.TechPhone,
|
||||
ApptDate = row.DispatchApptDate ?? row.ScheduledDate,
|
||||
ApptTime = WorkOrderDerivedFields.FormatApptTime(apptStart, apptEnd),
|
||||
Pm = row.Trade ?? row.Problem,
|
||||
Pm = row.Trade ?? row.ServiceNameSnapshot ?? row.Problem,
|
||||
ServiceNotes = row.ServiceNotes,
|
||||
ExtraServices = ParseExtraServices(row.ExtraServices),
|
||||
AdditionalContacts = MapAdditionalContacts(row),
|
||||
|
|
|
|||
|
|
@ -16,15 +16,18 @@ namespace SeaHaven.Services.Implementation
|
|||
private readonly IWorkOrderBoardDataService _boardDataService;
|
||||
private readonly IWorkOrderBoardMutationDataService _mutationData;
|
||||
private readonly IWorkOrderAuditService _auditService;
|
||||
private readonly IServicesRegistryService? _servicesRegistryService;
|
||||
|
||||
public WorkOrderBoardUpdateService(
|
||||
IWorkOrderBoardDataService boardDataService,
|
||||
IWorkOrderBoardMutationDataService mutationData,
|
||||
IWorkOrderAuditService auditService)
|
||||
IWorkOrderAuditService auditService,
|
||||
IServicesRegistryService? servicesRegistryService = null)
|
||||
{
|
||||
_boardDataService = boardDataService;
|
||||
_mutationData = mutationData;
|
||||
_auditService = auditService;
|
||||
_servicesRegistryService = servicesRegistryService;
|
||||
}
|
||||
|
||||
public async Task<WorkOrderBoardRowDto> PatchFieldAsync(
|
||||
|
|
@ -73,6 +76,13 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
_mutationData.SetExpectedWorkOrderVersion(workOrder, workOrderVersion);
|
||||
|
||||
if (canonicalField.Equals(WorkOrderBoardFieldNames.WorkOrderType, StringComparison.OrdinalIgnoreCase)
|
||||
&& workOrder.ServiceId.HasValue)
|
||||
{
|
||||
var nextType = ParseWorkOrderType(value: request.Value);
|
||||
await EnsureServiceSupportsTypeAsync(workOrder.ServiceId.Value, nextType, ct);
|
||||
}
|
||||
|
||||
Dispatch? dispatch = null;
|
||||
ResolvedDispatch? resolved = null;
|
||||
if (WorkOrderBoardFieldNames.IsDispatchField(canonicalField))
|
||||
|
|
@ -181,6 +191,7 @@ namespace SeaHaven.Services.Implementation
|
|||
{
|
||||
WorkOrderBoardFieldNames.WoNumber => new List<FieldChange> { await ApplyWoNumber(workOrder, value, auditField, cancellationToken) },
|
||||
WorkOrderBoardFieldNames.WorkOrderType => ApplyWorkOrderType(workOrder, value, auditField),
|
||||
WorkOrderBoardFieldNames.ServiceId => await ApplyServiceIdAsync(workOrder, value, auditField, cancellationToken),
|
||||
WorkOrderBoardFieldNames.Severity => new List<FieldChange> { ApplySeverity(workOrder, value, auditField) },
|
||||
WorkOrderBoardFieldNames.SiteCode => new List<FieldChange> { ApplyStringField(value, auditField, v => workOrder.SiteCode = v, () => workOrder.SiteCode) },
|
||||
WorkOrderBoardFieldNames.LifecycleStatus => ApplyLifecycleStatus(workOrder, value, auditField),
|
||||
|
|
@ -388,6 +399,83 @@ namespace SeaHaven.Services.Implementation
|
|||
return changes;
|
||||
}
|
||||
|
||||
private async Task<List<FieldChange>> ApplyServiceIdAsync(
|
||||
WorkOrder workOrder,
|
||||
string? value,
|
||||
string auditField,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_servicesRegistryService == null)
|
||||
throw new WorkOrderBoardValidationException("ServiceInvalid", "Service registry is unavailable.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
var previousServiceId = workOrder.ServiceId;
|
||||
var previousServiceLabel = workOrder.ServiceNameSnapshot ?? workOrder.Trade;
|
||||
workOrder.ServiceId = null;
|
||||
workOrder.ServiceNameSnapshot = null;
|
||||
workOrder.Trade = null;
|
||||
return new List<FieldChange>
|
||||
{
|
||||
FieldChange.ForField(auditField, previousServiceId?.ToString(), null),
|
||||
FieldChange.ForField("Trade", previousServiceLabel, null)
|
||||
};
|
||||
}
|
||||
|
||||
if (!int.TryParse(value.Trim(), out var serviceId) || serviceId <= 0)
|
||||
throw new WorkOrderBoardValidationException("ServiceInvalid", "serviceId must be a positive integer.");
|
||||
|
||||
var service = await _servicesRegistryService.GetByIdAsync(serviceId, cancellationToken);
|
||||
if (!ServicesRegistryService.AllowedWorkOrderTypes.Contains(workOrder.WorkOrderType ?? WorkOrderType.PM)
|
||||
|| service == null
|
||||
|| !service.IsActive
|
||||
|| !service.SupportedWorkOrderTypes.Contains(workOrder.WorkOrderType ?? WorkOrderType.PM))
|
||||
{
|
||||
throw new WorkOrderBoardValidationException(
|
||||
"ServiceInvalid",
|
||||
"serviceId must reference an active service that supports this work order type.");
|
||||
}
|
||||
|
||||
var oldId = workOrder.ServiceId;
|
||||
var oldLabel = workOrder.ServiceNameSnapshot ?? workOrder.Trade;
|
||||
workOrder.ServiceId = service.Id;
|
||||
workOrder.ServiceNameSnapshot = service.Name;
|
||||
workOrder.Trade = service.Name;
|
||||
return new List<FieldChange>
|
||||
{
|
||||
oldId == service.Id
|
||||
? FieldChange.Unchanged(auditField)
|
||||
: FieldChange.ForField(auditField, oldId?.ToString(), service.Id.ToString()),
|
||||
oldLabel == service.Name
|
||||
? FieldChange.Unchanged("Trade")
|
||||
: FieldChange.ForField("Trade", oldLabel, service.Name)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task EnsureServiceSupportsTypeAsync(int serviceId, WorkOrderType workOrderType, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_servicesRegistryService == null)
|
||||
throw new WorkOrderBoardValidationException("ServiceTypeInvalid", "Service registry is unavailable.");
|
||||
|
||||
var service = await _servicesRegistryService.GetByIdAsync(serviceId, cancellationToken);
|
||||
if (service == null || !service.SupportedWorkOrderTypes.Contains(workOrderType))
|
||||
throw new WorkOrderBoardValidationException(
|
||||
"ServiceTypeInvalid",
|
||||
"The selected service does not support this work order type.");
|
||||
}
|
||||
|
||||
private static WorkOrderType ParseWorkOrderType(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)
|
||||
|| string.Equals(value.Trim(), "Overdue", StringComparison.OrdinalIgnoreCase)
|
||||
|| !WorkOrderTypeMapper.TryParseFeLabel(value, out var parsed))
|
||||
{
|
||||
throw new WorkOrderBoardValidationException("InvalidValue", $"Invalid workOrderType: {value}");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private static FieldChange ApplySeverity(WorkOrder workOrder, string? value, string auditField)
|
||||
{
|
||||
if (!WorkOrderSeverityRules.TryNormalize(value, out var normalized))
|
||||
|
|
|
|||
|
|
@ -78,6 +78,28 @@ public class SH303ServicesRegistryMigrationTests
|
|||
Assert.Empty(operations.OfType<SqlOperation>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SH187_Up_AddsNullableServiceLinkAndBackfillsLegacyLabels()
|
||||
{
|
||||
var operations = BuildOperations<SH187_WorkOrderServiceLink>("Up");
|
||||
|
||||
var columns = operations.OfType<AddColumnOperation>().ToList();
|
||||
Assert.Contains(columns, c => c.Table == "workOrders" && c.Name == "ServiceId" && c.IsNullable);
|
||||
Assert.Contains(columns, c => c.Table == "workOrders" && c.Name == "ServiceNameSnapshot"
|
||||
&& c.IsNullable && c.MaxLength == 200);
|
||||
Assert.Contains(operations.OfType<CreateIndexOperation>(), i =>
|
||||
i.Table == "workOrders" && i.Columns.SequenceEqual(new[] { "ServiceId" }));
|
||||
Assert.Contains(operations.OfType<AddForeignKeyOperation>(), fk =>
|
||||
fk.Table == "workOrders"
|
||||
&& fk.PrincipalTable == "Services"
|
||||
&& fk.OnDelete == ReferentialAction.Restrict);
|
||||
|
||||
var sql = string.Join("\n", operations.OfType<SqlOperation>().Select(o => o.Sql));
|
||||
Assert.Contains("UPDATE w", sql);
|
||||
Assert.Contains("ServiceNameSnapshot", sql);
|
||||
Assert.Contains("LEFT JOIN [Services]", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Up_SeedsActiveProblemValuesIdempotently_WhenSqlServerAvailable()
|
||||
{
|
||||
|
|
@ -205,11 +227,15 @@ public class SH303ServicesRegistryMigrationTests
|
|||
}
|
||||
|
||||
private static List<MigrationOperation> BuildOperations(string direction)
|
||||
=> BuildOperations<SH303_ServicesRegistry>(direction);
|
||||
|
||||
private static List<MigrationOperation> BuildOperations<TMigration>(string direction)
|
||||
where TMigration : Migration, new()
|
||||
{
|
||||
var builder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer");
|
||||
var operation = typeof(Migration).GetMethod(direction, BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?? throw new InvalidOperationException($"Migration.{direction} not found.");
|
||||
operation.Invoke(new SH303_ServicesRegistry(), [builder]);
|
||||
operation.Invoke(new TMigration(), [builder]);
|
||||
return builder.Operations;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,24 @@ public class WorkOrderBoardServiceTests
|
|||
Assert.Equal("09:00", dto.ApptTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapRawRow_PmEditTakesPrecedenceOverServiceSnapshot()
|
||||
{
|
||||
var row = RawRow(
|
||||
scheduledDate: new DateTime(2026, 6, 23),
|
||||
scheduledStart: null,
|
||||
scheduledEnd: null,
|
||||
dispatchApptDate: null) with
|
||||
{
|
||||
Trade = "Updated service label",
|
||||
ServiceNameSnapshot = "Original service label"
|
||||
};
|
||||
|
||||
var dto = WorkOrderBoardService.MapRawRow(row, DateTime.UtcNow);
|
||||
|
||||
Assert.Equal("Updated service label", dto.Pm);
|
||||
}
|
||||
|
||||
private static WorkOrderBoardRawRow RawRow(
|
||||
DateTime? scheduledDate,
|
||||
DateTime? scheduledStart,
|
||||
|
|
@ -1055,6 +1073,8 @@ public class WorkOrderBoardServiceTests
|
|||
CarriedOver: 0,
|
||||
IsAddOn: false,
|
||||
WorkOrderType: null,
|
||||
ServiceId: null,
|
||||
ServiceNameSnapshot: null,
|
||||
SiteCode: null,
|
||||
LocationName: null,
|
||||
PocName: null,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue