shoc-backend/SeaHaven.Services/Helpers/WorkOrderAuditProjection.cs
Arthur Bassi f4985378fc
feat(work-orders): add structured service notes POC and vendor fields (#21)
* fix(migrations): make Phase6 completion slide-over migration idempotent

Use IF EXISTS / IF COL_LENGTH guards and explicit EF migration metadata
so partial environments can apply Phase 6 safely.

* feat(locations): add sites options endpoint and vendor contactName

Expose GET /api/locations/sites for the WO create wizard and include
vendor contactName on dropdown responses.

* feat(work-orders): add structured service notes POC and vendor fields

Persist primary/extra services, service notes, free-text POC, and
tech/vendor notes on create/patch/board/detail instead of embedding
them in description.

* fix(work-orders): make stacked tip build for CI quality gates

* fix(work-orders): align stacked services with CI build

* fix(work-orders): keep FlagColor out of structured-notes slice

Remove FlagColor model/API/tests from this PR so schema and EF model stay aligned with the structured-fields migration. FlagColor remains in the follow-on PR that adds its migration.

---------

Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com>
2026-07-22 17:36:46 -03:00

87 lines
3 KiB
C#

using Data.SeaHavenIndustries;
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Helpers
{
public static class WorkOrderAuditProjection
{
private const string UnassignedLabel = "Unassigned";
public static WorkOrderAuditEntryDto ToDto(
WorkOrderAuditLog log,
IReadOnlyDictionary<string, string>? assignToDisplayNames = null)
{
var oldValue = log.OldValue ?? "";
var newValue = log.NewValue ?? "";
if (IsAssignToField(log.FieldName))
{
oldValue = ResolveAssignToDisplay(oldValue, assignToDisplayNames);
newValue = ResolveAssignToDisplay(newValue, assignToDisplayNames);
}
return new WorkOrderAuditEntryDto
{
Type = MapEventType(log.EventType),
DispatcherId = log.UserId,
Action = log.Action ?? "",
FieldName = log.FieldName,
OldValue = oldValue,
NewValue = newValue,
Time = log.CreatedAt.ToString("o"),
DispatchId = log.DispatchId
};
}
public static List<WorkOrderAuditEntryDto> ToDtos(
IEnumerable<WorkOrderAuditLog> logs,
IReadOnlyDictionary<string, string>? assignToDisplayNames = null)
=> logs.Select(log => ToDto(log, assignToDisplayNames)).ToList();
public static IEnumerable<string> CollectAssignToUserIds(IEnumerable<WorkOrderAuditLog> logs)
{
return logs
.Where(l => IsAssignToField(l.FieldName))
.SelectMany(l => new[] { l.OldValue, l.NewValue })
.Where(v => !string.IsNullOrWhiteSpace(v))
.Select(v => v!)
.Distinct(StringComparer.Ordinal);
}
internal static bool IsAssignToField(string? fieldName)
=> string.Equals(fieldName, "AssignTo", StringComparison.OrdinalIgnoreCase)
|| string.Equals(fieldName, "AssignedTo", StringComparison.OrdinalIgnoreCase);
private static string ResolveAssignToDisplay(
string value,
IReadOnlyDictionary<string, string>? assignToDisplayNames)
{
if (string.IsNullOrWhiteSpace(value))
return UnassignedLabel;
if (assignToDisplayNames != null
&& assignToDisplayNames.TryGetValue(value, out var name)
&& !string.IsNullOrWhiteSpace(name))
{
return name;
}
return value;
}
private static string MapEventType(string? eventType)
{
if (string.IsNullOrWhiteSpace(eventType))
return "manual";
return eventType.ToLowerInvariant() switch
{
"manual" => "manual",
"system" => "system",
"sync" => "sync",
"vendor" => "vendor",
_ => eventType.ToLowerInvariant()
};
}
}
}