mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
Merge pull request #113 from Sea-Haven-Industries/feat/SH-191-completion-freeze
Some checks failed
Some checks failed
feat(work-orders): capture Site/Vendor/POC snapshot on Completed
This commit is contained in:
commit
eb9b7eb50e
14 changed files with 634 additions and 11 deletions
|
|
@ -0,0 +1,41 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260910143000_WorkOrderCompletionSnapshot")]
|
||||
public partial class WorkOrderCompletionSnapshot : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "FrozenCompany",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "FrozenPoc",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "FrozenSite",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "FrozenCompany", table: "workOrders");
|
||||
migrationBuilder.DropColumn(name: "FrozenPoc", table: "workOrders");
|
||||
migrationBuilder.DropColumn(name: "FrozenSite", table: "workOrders");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2494,6 +2494,15 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
.HasMaxLength(7)
|
||||
.HasColumnType("nvarchar(7)");
|
||||
|
||||
b.Property<string>("FrozenCompany")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("FrozenPoc")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("FrozenSite")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("InternalWONumber")
|
||||
.HasMaxLength(11)
|
||||
.HasColumnType("nvarchar(11)");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
namespace Data.SeaHavenIndustries
|
||||
{
|
||||
public sealed class WorkOrderFrozenSite
|
||||
{
|
||||
public string? Code { get; set; }
|
||||
public string? Label { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? State { get; set; }
|
||||
public string? Zip { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Maps { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WorkOrderFrozenCompany
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? ContactName { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? State { get; set; }
|
||||
public string? Zip { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Maps { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WorkOrderFrozenPoc
|
||||
{
|
||||
public List<WorkOrderFrozenPocContact> Contacts { get; set; } = new();
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WorkOrderFrozenPocContact
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -93,6 +93,12 @@ namespace Data.SeaHavenIndustries
|
|||
|
||||
// --- Completion slice ---
|
||||
public DocStatus? DocStatus { get; set; }
|
||||
/// <summary>JSON site snapshot captured on the first transition to Completed.</summary>
|
||||
public string? FrozenSite { get; set; }
|
||||
/// <summary>JSON live-vendor snapshot captured on the first transition to Completed.</summary>
|
||||
public string? FrozenCompany { get; set; }
|
||||
/// <summary>JSON effective POC snapshot captured on the first transition to Completed.</summary>
|
||||
public string? FrozenPoc { get; set; }
|
||||
|
||||
/// <summary>Board flag color (#RRGGBB from fixed palette). Null = no flag.</summary>
|
||||
[MaxLength(7)]
|
||||
|
|
|
|||
|
|
@ -2,11 +2,15 @@ using Data.SeaHavenIndustries;
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SeaHaven.DataServices.Helpers
|
||||
{
|
||||
public static class WorkOrderBoardProjection
|
||||
{
|
||||
private static readonly JsonSerializerOptions SnapshotJsonOptions =
|
||||
new(JsonSerializerDefaults.Web);
|
||||
|
||||
public static async Task<IReadOnlyList<WorkOrderBoardRawRow>> ProjectRowsAsync(
|
||||
ApplicationDbContext context,
|
||||
IQueryable<WorkOrder> query,
|
||||
|
|
@ -81,6 +85,9 @@ namespace SeaHaven.DataServices.Helpers
|
|||
w.AdditionalContacts,
|
||||
w.DocStatus,
|
||||
w.CompletedDate,
|
||||
w.FrozenSite,
|
||||
w.FrozenCompany,
|
||||
w.FrozenPoc,
|
||||
w.FlagColor,
|
||||
w.PrimaryDispatchId,
|
||||
w.RowVersion,
|
||||
|
|
@ -96,9 +103,22 @@ namespace SeaHaven.DataServices.Helpers
|
|||
|
||||
return rows.Select(w =>
|
||||
{
|
||||
var pocName = !string.IsNullOrWhiteSpace(w.WoPocName) ? w.WoPocName : w.ContactPoc?.Name;
|
||||
var pocPhone = !string.IsNullOrWhiteSpace(w.WoPocPhone) ? w.WoPocPhone : w.ContactPoc?.PhoneNumber;
|
||||
var pocNotes = !string.IsNullOrWhiteSpace(w.WoPocNotes) ? w.WoPocNotes : w.ContactPoc?.Notes;
|
||||
var frozenSite = w.LifecycleStatus == LifecycleStatus.Completed
|
||||
? Deserialize<WorkOrderFrozenSite>(w.FrozenSite)
|
||||
: null;
|
||||
var frozenCompany = w.LifecycleStatus == LifecycleStatus.Completed
|
||||
? Deserialize<WorkOrderFrozenCompany>(w.FrozenCompany)
|
||||
: null;
|
||||
var frozenPoc = w.LifecycleStatus == LifecycleStatus.Completed
|
||||
? Deserialize<WorkOrderFrozenPoc>(w.FrozenPoc)
|
||||
: null;
|
||||
var primaryFrozenPoc = frozenPoc?.Contacts.FirstOrDefault();
|
||||
var pocName = primaryFrozenPoc?.Name
|
||||
?? (!string.IsNullOrWhiteSpace(w.WoPocName) ? w.WoPocName : w.ContactPoc?.Name);
|
||||
var pocPhone = primaryFrozenPoc?.Phone
|
||||
?? (!string.IsNullOrWhiteSpace(w.WoPocPhone) ? w.WoPocPhone : w.ContactPoc?.PhoneNumber);
|
||||
var pocNotes = frozenPoc?.Notes
|
||||
?? (!string.IsNullOrWhiteSpace(w.WoPocNotes) ? w.WoPocNotes : w.ContactPoc?.Notes);
|
||||
var techPhone = !string.IsNullOrWhiteSpace(w.DispatchTechPhone)
|
||||
? w.DispatchTechPhone
|
||||
: (!string.IsNullOrWhiteSpace(w.WoTechPhone) ? w.WoTechPhone : w.VendorPhone);
|
||||
|
|
@ -106,6 +126,7 @@ namespace SeaHaven.DataServices.Helpers
|
|||
w.VendorId,
|
||||
w.VendorName,
|
||||
w.PrimaryDispatchStatus);
|
||||
vendorName = frozenCompany?.Name ?? vendorName;
|
||||
upliftByWorkOrder.TryGetValue(w.Id, out var uplift);
|
||||
|
||||
return new WorkOrderBoardRawRow(
|
||||
|
|
@ -115,8 +136,8 @@ namespace SeaHaven.DataServices.Helpers
|
|||
w.CarriedOver,
|
||||
w.IsAddOn,
|
||||
w.WorkOrderType,
|
||||
w.SiteCode,
|
||||
w.LocationName,
|
||||
frozenSite?.Code ?? w.SiteCode,
|
||||
frozenSite?.Label ?? w.LocationName,
|
||||
pocName,
|
||||
pocPhone,
|
||||
pocNotes,
|
||||
|
|
@ -135,8 +156,8 @@ namespace SeaHaven.DataServices.Helpers
|
|||
w.ScheduleWeekOnly,
|
||||
vendorId,
|
||||
vendorName,
|
||||
w.TechName,
|
||||
techPhone,
|
||||
frozenCompany?.ContactName ?? w.TechName,
|
||||
frozenCompany?.Phone ?? techPhone,
|
||||
w.DispatchApptDate,
|
||||
w.Trade ?? w.Problem,
|
||||
w.Problem,
|
||||
|
|
@ -160,10 +181,28 @@ namespace SeaHaven.DataServices.Helpers
|
|||
w.OriginalWeek,
|
||||
isUnscheduled,
|
||||
w.Severity,
|
||||
w.MediaCount);
|
||||
w.MediaCount,
|
||||
frozenSite,
|
||||
frozenCompany,
|
||||
frozenPoc);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private static T? Deserialize<T>(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return default;
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(json, SnapshotJsonOptions);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
|
|
|||
|
|
@ -20,7 +20,13 @@ namespace SeaHaven.DataServices.Implementation
|
|||
=> await _context.workOrders.MaxAsync(w => (int?)w.Id, cancellationToken) ?? 0;
|
||||
|
||||
public Task<WorkOrder?> GetTrackedWorkOrderAsync(int workOrderId, CancellationToken cancellationToken)
|
||||
=> _context.workOrders.FirstOrDefaultAsync(
|
||||
=> _context.workOrders
|
||||
.Include(workOrder => workOrder.Locations)
|
||||
.Include(workOrder => workOrder.WorkOrderContacts!)
|
||||
.ThenInclude(contact => contact.POC)
|
||||
.Include(workOrder => workOrder.PrimaryDispatch!)
|
||||
.ThenInclude(dispatch => dispatch.Vendor)
|
||||
.FirstOrDefaultAsync(
|
||||
w => w.Id == workOrderId && w.istemplate != true && (w.IsDeleted != true || w.IsDeleted == null),
|
||||
cancellationToken);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
|
|
@ -65,7 +66,10 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
DateOnly? OriginalWeek,
|
||||
bool IsUnscheduled,
|
||||
string? Severity = null,
|
||||
int MediaCount = 0);
|
||||
int MediaCount = 0,
|
||||
WorkOrderFrozenSite? FrozenSite = null,
|
||||
WorkOrderFrozenCompany? FrozenCompany = null,
|
||||
WorkOrderFrozenPoc? FrozenPoc = null);
|
||||
|
||||
public record WorkOrderBoardQueryResult(
|
||||
IReadOnlyList<WorkOrderBoardRawRow> ScheduledRows,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,44 @@ namespace SeaHaven.Services.DTOs
|
|||
public decimal? Amount { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderFrozenSiteDto
|
||||
{
|
||||
public string? Code { get; set; }
|
||||
public string? Label { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? State { get; set; }
|
||||
public string? Zip { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Maps { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderFrozenCompanyDto
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? ContactName { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? State { get; set; }
|
||||
public string? Zip { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Maps { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderFrozenPocDto
|
||||
{
|
||||
public List<WorkOrderFrozenPocContactDto> Contacts { get; set; } = new();
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderFrozenPocContactDto
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderBoardRowDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -53,6 +91,9 @@ namespace SeaHaven.Services.DTOs
|
|||
public string? PocPhone { get; set; }
|
||||
public string? PocNotes { get; set; }
|
||||
public List<WorkOrderAdditionalContactDto>? AdditionalContacts { get; set; }
|
||||
public WorkOrderFrozenSiteDto? FrozenSite { get; set; }
|
||||
public WorkOrderFrozenCompanyDto? FrozenCompany { get; set; }
|
||||
public WorkOrderFrozenPocDto? FrozenPoc { get; set; }
|
||||
public LifecycleStatus? LifecycleStatus { get; set; }
|
||||
public string? LifecycleStatusLabel { get; set; }
|
||||
public string? LegacyStatus { get; set; }
|
||||
|
|
|
|||
151
SeaHaven.Services/Helpers/WorkOrderCompletionSnapshotMapper.cs
Normal file
151
SeaHaven.Services/Helpers/WorkOrderCompletionSnapshotMapper.cs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Data.SeaHavenIndustries;
|
||||
using SeaHaven.DataServices.Helpers;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderCompletionSnapshotMapper
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public static void Capture(WorkOrder workOrder)
|
||||
{
|
||||
var location = workOrder.Locations;
|
||||
|
||||
var contact = workOrder.WorkOrderContacts?
|
||||
.OrderBy(item => item.Id)
|
||||
.FirstOrDefault();
|
||||
var pocName = FirstNotBlank(
|
||||
workOrder.PocName,
|
||||
JoinName(contact?.POC?.FirstName, contact?.POC?.MiddleName, contact?.POC?.LastName));
|
||||
var pocPhone = FirstNotBlank(workOrder.PocPhone, contact?.POC?.PhoneNumber);
|
||||
var pocNotes = FirstNotBlank(workOrder.PocNotes, contact?.Notes);
|
||||
|
||||
var contacts = new List<WorkOrderFrozenPocContact>();
|
||||
AddContact(contacts, pocName, pocPhone);
|
||||
foreach (var additional in WorkOrderAdditionalContactsMapper.ParseJson(workOrder.AdditionalContacts)
|
||||
?? Enumerable.Empty<WorkOrderAdditionalContactDto>())
|
||||
{
|
||||
AddContact(contacts, additional.Name, additional.Phone);
|
||||
}
|
||||
|
||||
workOrder.FrozenSite = JsonSerializer.Serialize(
|
||||
new WorkOrderFrozenSite
|
||||
{
|
||||
Code = workOrder.SiteCode,
|
||||
Label = FirstNotBlank(location?.Name, location?.Title, workOrder.SiteCode),
|
||||
Address = JoinAddress(location?.Address1, location?.Address2),
|
||||
City = location?.City,
|
||||
State = location?.State,
|
||||
Zip = location?.Zip,
|
||||
Phone = location?.PhoneNumber,
|
||||
Email = location?.Email,
|
||||
Maps = location == null ? null : BuildMapsUrl(location)
|
||||
},
|
||||
JsonOptions);
|
||||
|
||||
var liveVendor = ResolveLiveVendor(workOrder.PrimaryDispatch);
|
||||
workOrder.FrozenCompany = liveVendor == null
|
||||
? null
|
||||
: JsonSerializer.Serialize(
|
||||
new WorkOrderFrozenCompany
|
||||
{
|
||||
Name = liveVendor.CompanyName,
|
||||
ContactName = liveVendor.ContactName,
|
||||
Address = liveVendor.Address,
|
||||
City = liveVendor.City,
|
||||
State = liveVendor.State,
|
||||
Zip = liveVendor.Zip,
|
||||
Phone = FirstNotBlank(liveVendor.CompanyPhone, liveVendor.Phone),
|
||||
Email = liveVendor.Email,
|
||||
Maps = liveVendor.GoogleMapsUrl
|
||||
},
|
||||
JsonOptions);
|
||||
|
||||
workOrder.FrozenPoc = JsonSerializer.Serialize(
|
||||
new WorkOrderFrozenPoc
|
||||
{
|
||||
Contacts = contacts,
|
||||
Notes = pocNotes
|
||||
},
|
||||
JsonOptions);
|
||||
}
|
||||
|
||||
public static WorkOrderFrozenSite? ParseSite(string? json)
|
||||
=> Deserialize<WorkOrderFrozenSite>(json);
|
||||
|
||||
public static WorkOrderFrozenCompany? ParseCompany(string? json)
|
||||
=> Deserialize<WorkOrderFrozenCompany>(json);
|
||||
|
||||
public static WorkOrderFrozenPoc? ParsePoc(string? json)
|
||||
=> Deserialize<WorkOrderFrozenPoc>(json);
|
||||
|
||||
private static Vendor? ResolveLiveVendor(Dispatch? dispatch)
|
||||
{
|
||||
var (vendorId, _) = WorkOrderBoardDispatchAssignment.LiveVendor(
|
||||
dispatch?.VendorId,
|
||||
dispatch?.Vendor?.CompanyName,
|
||||
dispatch?.Status);
|
||||
return vendorId.HasValue ? dispatch?.Vendor : null;
|
||||
}
|
||||
|
||||
private static string? BuildMapsUrl(Locations location)
|
||||
{
|
||||
if (!double.TryParse(location.Latitude, NumberStyles.Float, CultureInfo.InvariantCulture, out var latitude)
|
||||
|| !double.TryParse(location.Longitude, NumberStyles.Float, CultureInfo.InvariantCulture, out var longitude))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return FormattableString.Invariant(
|
||||
$"https://www.google.com/maps/search/?api=1&query={latitude},{longitude}");
|
||||
}
|
||||
|
||||
private static string? JoinAddress(params string?[] parts)
|
||||
{
|
||||
var value = string.Join(", ", parts.Where(part => !string.IsNullOrWhiteSpace(part)));
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
|
||||
private static string? JoinName(params string?[] parts)
|
||||
{
|
||||
var value = string.Join(" ", parts.Where(part => !string.IsNullOrWhiteSpace(part)));
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
|
||||
private static string? FirstNotBlank(params string?[] values)
|
||||
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim();
|
||||
|
||||
private static void AddContact(
|
||||
ICollection<WorkOrderFrozenPocContact> contacts,
|
||||
string? name,
|
||||
string? phone)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(phone))
|
||||
return;
|
||||
|
||||
contacts.Add(new WorkOrderFrozenPocContact
|
||||
{
|
||||
Name = name?.Trim(),
|
||||
Phone = phone?.Trim()
|
||||
});
|
||||
}
|
||||
|
||||
private static T? Deserialize<T>(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return default;
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(json, JsonOptions);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Security.Claims;
|
||||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.DataServices.Helpers;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
|
@ -113,6 +114,9 @@ namespace SeaHaven.Services.Implementation
|
|||
PocName = row.PocName,
|
||||
PocPhone = row.PocPhone,
|
||||
PocNotes = row.PocNotes,
|
||||
FrozenSite = MapFrozenSite(row.FrozenSite),
|
||||
FrozenCompany = MapFrozenCompany(row.FrozenCompany),
|
||||
FrozenPoc = MapFrozenPoc(row.FrozenPoc),
|
||||
LifecycleStatus = row.LifecycleStatus,
|
||||
LifecycleStatusLabel = WorkOrderDerivedFields.GetLifecycleStatusLabel(row.LifecycleStatus),
|
||||
LegacyStatus = row.LegacyStatus,
|
||||
|
|
@ -135,7 +139,7 @@ namespace SeaHaven.Services.Implementation
|
|||
Pm = row.Trade ?? row.Problem,
|
||||
ServiceNotes = row.ServiceNotes,
|
||||
ExtraServices = ParseExtraServices(row.ExtraServices),
|
||||
AdditionalContacts = WorkOrderAdditionalContactsMapper.ParseJson(row.AdditionalContacts),
|
||||
AdditionalContacts = MapAdditionalContacts(row),
|
||||
DocStatus = row.DocStatus,
|
||||
MediaCount = row.MediaCount,
|
||||
CompletedDate = row.CompletedDate,
|
||||
|
|
@ -149,6 +153,71 @@ namespace SeaHaven.Services.Implementation
|
|||
};
|
||||
}
|
||||
|
||||
private static List<WorkOrderAdditionalContactDto>? MapAdditionalContacts(
|
||||
WorkOrderBoardRawRow row)
|
||||
{
|
||||
if (row.FrozenPoc == null)
|
||||
return WorkOrderAdditionalContactsMapper.ParseJson(row.AdditionalContacts);
|
||||
|
||||
var additional = row.FrozenPoc.Contacts
|
||||
.Skip(1)
|
||||
.Where(contact => !string.IsNullOrWhiteSpace(contact.Name)
|
||||
|| !string.IsNullOrWhiteSpace(contact.Phone))
|
||||
.Select(contact => new WorkOrderAdditionalContactDto(
|
||||
contact.Name ?? "",
|
||||
contact.Phone ?? "",
|
||||
null))
|
||||
.ToList();
|
||||
return additional.Count > 0 ? additional : null;
|
||||
}
|
||||
|
||||
private static WorkOrderFrozenSiteDto? MapFrozenSite(WorkOrderFrozenSite? site)
|
||||
=> site == null
|
||||
? null
|
||||
: new WorkOrderFrozenSiteDto
|
||||
{
|
||||
Code = site.Code,
|
||||
Label = site.Label,
|
||||
Address = site.Address,
|
||||
City = site.City,
|
||||
State = site.State,
|
||||
Zip = site.Zip,
|
||||
Phone = site.Phone,
|
||||
Email = site.Email,
|
||||
Maps = site.Maps
|
||||
};
|
||||
|
||||
private static WorkOrderFrozenCompanyDto? MapFrozenCompany(WorkOrderFrozenCompany? company)
|
||||
=> company == null
|
||||
? null
|
||||
: new WorkOrderFrozenCompanyDto
|
||||
{
|
||||
Name = company.Name,
|
||||
ContactName = company.ContactName,
|
||||
Address = company.Address,
|
||||
City = company.City,
|
||||
State = company.State,
|
||||
Zip = company.Zip,
|
||||
Phone = company.Phone,
|
||||
Email = company.Email,
|
||||
Maps = company.Maps
|
||||
};
|
||||
|
||||
private static WorkOrderFrozenPocDto? MapFrozenPoc(WorkOrderFrozenPoc? poc)
|
||||
=> poc == null
|
||||
? null
|
||||
: new WorkOrderFrozenPocDto
|
||||
{
|
||||
Notes = poc.Notes,
|
||||
Contacts = poc.Contacts
|
||||
.Select(contact => new WorkOrderFrozenPocContactDto
|
||||
{
|
||||
Name = contact.Name,
|
||||
Phone = contact.Phone
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
|
||||
private static bool HasClockTime(DateTime? value)
|
||||
=> value.HasValue && value.Value.TimeOfDay != TimeSpan.Zero;
|
||||
|
||||
|
|
|
|||
|
|
@ -530,6 +530,9 @@ namespace SeaHaven.Services.Implementation
|
|||
var old = workOrder.LifecycleStatus?.ToString() ?? workOrder.Status;
|
||||
if (workOrder.LifecycleStatus != parsed)
|
||||
{
|
||||
if (parsed == LifecycleStatus.Completed)
|
||||
WorkOrderCompletionSnapshotMapper.Capture(workOrder);
|
||||
|
||||
workOrder.LifecycleStatus = parsed;
|
||||
workOrder.Status = WorkOrderDerivedFields.GetLifecycleStatusLabel(parsed);
|
||||
if (workOrder.LegacyStatus == null && workOrder.Status != null)
|
||||
|
|
|
|||
|
|
@ -106,6 +106,9 @@ namespace SeaHaven.Services.Implementation
|
|||
PocPhone = row.PocPhone,
|
||||
PocNotes = row.PocNotes,
|
||||
AdditionalContacts = row.AdditionalContacts,
|
||||
FrozenSite = row.FrozenSite,
|
||||
FrozenCompany = row.FrozenCompany,
|
||||
FrozenPoc = row.FrozenPoc,
|
||||
LifecycleStatus = row.LifecycleStatus,
|
||||
LifecycleStatusLabel = row.LifecycleStatusLabel,
|
||||
LegacyStatus = row.LegacyStatus,
|
||||
|
|
|
|||
|
|
@ -397,11 +397,13 @@ public class WorkOrderBoardUpdateServiceTests
|
|||
var wo = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
LocationId = 10,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
DueDate = DateTime.UtcNow.Date.AddDays(-3),
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(-1),
|
||||
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||
};
|
||||
context.Locations.Add(new Locations { Id = 10, Name = "Test Site" });
|
||||
context.workOrders.Add(wo);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
|
|
@ -435,12 +437,14 @@ public class WorkOrderBoardUpdateServiceTests
|
|||
var wo = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
LocationId = 10,
|
||||
InternalWONumber = "SH00001",
|
||||
WorkerOrderNumber = "SH00001",
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(1),
|
||||
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||
};
|
||||
context.Locations.Add(new Locations { Id = 10, Name = "Test Site" });
|
||||
context.workOrders.Add(wo);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
|
|
|
|||
207
SeaHavenIndustries.Tests/WorkOrderCompletionFreezeTests.cs
Normal file
207
SeaHavenIndustries.Tests/WorkOrderCompletionFreezeTests.cs
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Implementation;
|
||||
using SeaHaven.Services.Constants;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Helpers;
|
||||
using SeaHaven.Services.Implementation;
|
||||
|
||||
namespace SeaHavenIndustries.Tests;
|
||||
|
||||
public class WorkOrderCompletionFreezeTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task FirstCompletedTransitionCapturesSnapshotAndLaterPatchKeepsIt()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var service = CreateService(context);
|
||||
var location = new Locations
|
||||
{
|
||||
Id = 10,
|
||||
Name = "BK5 Chicago",
|
||||
Address1 = "100 Main St",
|
||||
City = "Chicago",
|
||||
State = "IL",
|
||||
Zip = "60601",
|
||||
PhoneNumber = "312-555-0100",
|
||||
Email = "site@example.com"
|
||||
};
|
||||
var vendor = new Vendor
|
||||
{
|
||||
Id = 20,
|
||||
CompanyName = "Frozen Vendor",
|
||||
ContactName = "Frozen Tech",
|
||||
Address = "200 Vendor Ave",
|
||||
City = "Chicago",
|
||||
State = "IL",
|
||||
Zip = "60602",
|
||||
CompanyPhone = "312-555-0200",
|
||||
Email = "vendor@example.com"
|
||||
};
|
||||
var contact = new Contacts
|
||||
{
|
||||
Id = 30,
|
||||
FirstName = "Frozen",
|
||||
LastName = "POC",
|
||||
PhoneNumber = "312-555-0300"
|
||||
};
|
||||
var dispatch = new Dispatch
|
||||
{
|
||||
Id = 40,
|
||||
WorkOrderId = 1,
|
||||
VendorId = vendor.Id,
|
||||
Vendor = vendor,
|
||||
Status = "Sent"
|
||||
};
|
||||
var workOrder = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
InternalWONumber = "00000012345",
|
||||
SiteCode = "BK5",
|
||||
LocationId = location.Id,
|
||||
Locations = location,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(1),
|
||||
PrimaryDispatchId = dispatch.Id,
|
||||
PrimaryDispatch = dispatch,
|
||||
PocNotes = "Call first",
|
||||
AdditionalContacts = """[{"name":"Backup","phone":"312-555-0400"}]""",
|
||||
RowVersion = Version()
|
||||
};
|
||||
var workOrderContact = new WorkOrderContacts
|
||||
{
|
||||
Id = 50,
|
||||
WorkorderId = workOrder.Id,
|
||||
WorkOrder = workOrder,
|
||||
ContactId = contact.Id,
|
||||
POC = contact
|
||||
};
|
||||
workOrder.WorkOrderContacts = new[] { workOrderContact };
|
||||
context.AddRange(location, vendor, contact, dispatch, workOrder, workOrderContact);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var completed = await service.PatchFieldAsync(
|
||||
workOrder.Id,
|
||||
Patch(WorkOrderBoardFieldNames.LifecycleStatus, "Complete", workOrder),
|
||||
"actor-1");
|
||||
var persisted = await context.workOrders.SingleAsync();
|
||||
var frozenSiteJson = persisted.FrozenSite;
|
||||
var frozenCompanyJson = persisted.FrozenCompany;
|
||||
var frozenPocJson = persisted.FrozenPoc;
|
||||
|
||||
Assert.Equal(LifecycleStatus.Completed, completed.LifecycleStatus);
|
||||
Assert.Equal("BK5 Chicago", completed.LocationName);
|
||||
Assert.Equal("Frozen Vendor", completed.VendorName);
|
||||
Assert.Equal("Frozen POC", completed.PocName);
|
||||
Assert.Equal("Chicago", completed.FrozenSite?.City);
|
||||
Assert.Equal("Frozen Vendor", completed.FrozenCompany?.Name);
|
||||
Assert.Equal(2, completed.FrozenPoc?.Contacts.Count);
|
||||
|
||||
location.Name = "Changed Site";
|
||||
location.City = "Milwaukee";
|
||||
vendor.CompanyName = "Changed Vendor";
|
||||
contact.FirstName = "Changed";
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var afterFlag = await service.PatchFieldAsync(
|
||||
workOrder.Id,
|
||||
Patch(WorkOrderBoardFieldNames.FlagColor, "#EF4444", persisted),
|
||||
"actor-1");
|
||||
|
||||
Assert.Equal(frozenSiteJson, persisted.FrozenSite);
|
||||
Assert.Equal(frozenCompanyJson, persisted.FrozenCompany);
|
||||
Assert.Equal(frozenPocJson, persisted.FrozenPoc);
|
||||
Assert.Equal("BK5 Chicago", afterFlag.LocationName);
|
||||
Assert.Equal("Frozen Vendor", afterFlag.VendorName);
|
||||
Assert.Equal("Frozen POC", afterFlag.PocName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FirstCompletedTransitionWithoutSiteStillCapturesAvailableSnapshot()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var service = CreateService(context);
|
||||
var workOrder = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
InternalWONumber = "00000012345",
|
||||
SiteCode = "BK5",
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(1),
|
||||
RowVersion = Version()
|
||||
};
|
||||
context.Add(workOrder);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var completed = await service.PatchFieldAsync(
|
||||
workOrder.Id,
|
||||
Patch(WorkOrderBoardFieldNames.LifecycleStatus, "Complete", workOrder),
|
||||
"actor-1");
|
||||
var persisted = await context.workOrders.SingleAsync();
|
||||
|
||||
Assert.Equal(LifecycleStatus.Completed, completed.LifecycleStatus);
|
||||
Assert.Equal("BK5", completed.FrozenSite?.Code);
|
||||
Assert.Equal("BK5", completed.FrozenSite?.Label);
|
||||
Assert.Null(completed.FrozenSite?.City);
|
||||
Assert.Null(completed.FrozenCompany);
|
||||
Assert.False(string.IsNullOrWhiteSpace(persisted.FrozenSite));
|
||||
Assert.Null(persisted.FrozenCompany);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenWorkOrderProjectionKeepsUsingLiveLocation()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var location = new Locations { Id = 10, Name = "Live Site", City = "Chicago" };
|
||||
var workOrder = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
SiteCode = "BK5",
|
||||
LocationId = location.Id,
|
||||
Locations = location,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(1),
|
||||
FrozenSite = """{"code":"BK5","label":"Frozen Site","city":"Old City"}""",
|
||||
RowVersion = Version()
|
||||
};
|
||||
context.AddRange(location, workOrder);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var raw = await new WorkOrderBoardDataService(context).GetBoardRowByIdAsync(workOrder.Id);
|
||||
var dto = WorkOrderBoardService.MapRawRow(Assert.IsType<SeaHaven.DataServices.Interfaces.WorkOrderBoardRawRow>(raw), DateTime.UtcNow);
|
||||
|
||||
Assert.Equal("Live Site", dto.LocationName);
|
||||
Assert.Null(dto.FrozenSite);
|
||||
}
|
||||
|
||||
private static ApplicationDbContext CreateContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new ApplicationDbContext(options);
|
||||
}
|
||||
|
||||
private static WorkOrderBoardUpdateService CreateService(ApplicationDbContext context)
|
||||
{
|
||||
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
||||
return new WorkOrderBoardUpdateService(
|
||||
new WorkOrderBoardDataService(context),
|
||||
new WorkOrderBoardMutationDataService(context),
|
||||
new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks));
|
||||
}
|
||||
|
||||
private static WorkOrderBoardPatchRequestDto Patch(
|
||||
string field,
|
||||
string value,
|
||||
WorkOrder workOrder)
|
||||
=> new()
|
||||
{
|
||||
Field = field,
|
||||
Value = value,
|
||||
WorkOrderVersion = Convert.ToBase64String(workOrder.RowVersion ?? Version())
|
||||
};
|
||||
|
||||
private static byte[] Version() => new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 };
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue