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.
This commit is contained in:
Arthur Bassi 2026-07-21 09:38:34 -03:00
parent d59f92910e
commit 33574c233b
11 changed files with 1 additions and 241 deletions

View file

@ -1975,10 +1975,6 @@ namespace Data.SeaHavenIndustries.Migrations
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<string>("FlagColor")
.HasMaxLength(7)
.HasColumnType("nvarchar(7)");
b.Property<string>("InternalWONumber")
.HasColumnType("nvarchar(max)");

View file

@ -70,10 +70,6 @@ namespace Data.SeaHavenIndustries
// --- Completion slice ---
public DocStatus? DocStatus { get; set; }
/// <summary>Board flag color (#RRGGBB from fixed palette). Null = no flag.</summary>
[MaxLength(7)]
public string? FlagColor { get; set; }
// --- Legacy SQL columns (drift reconciliation) ---
public string? AvettaTask { get; set; }
public DateOnly? AssignDate { get; set; }

View file

@ -66,7 +66,6 @@ namespace SeaHaven.DataServices.Helpers
w.ServiceNotes,
w.ExtraServices,
w.DocStatus,
w.FlagColor,
w.PrimaryDispatchId,
w.RowVersion,
DispatchRowVersion = w.PrimaryDispatch != null ? w.PrimaryDispatch.RowVersion : null
@ -116,7 +115,6 @@ namespace SeaHaven.DataServices.Helpers
w.ServiceNotes,
w.ExtraServices,
w.DocStatus,
w.FlagColor,
w.PrimaryDispatchId,
w.RowVersion,
w.DispatchRowVersion,

View file

@ -45,7 +45,6 @@ namespace SeaHaven.DataServices.Interfaces
string? ServiceNotes,
string? ExtraServices,
DocStatus? DocStatus,
string? FlagColor,
int? PrimaryDispatchId,
byte[]? RowVersion,
byte[]? DispatchRowVersion,

View file

@ -54,8 +54,6 @@ namespace SeaHaven.Services.DTOs
public string? ServiceNotes { get; set; }
public List<string>? ExtraServices { get; set; }
public DocStatus? DocStatus { get; set; }
/// <summary>Board flag color (#RRGGBB). Null = no flag. Distinct from Color (dispatcher avatar).</summary>
public string? FlagColor { get; set; }
public int? PrimaryDispatchId { get; set; }
public byte[]? RowVersion { get; set; }
public byte[]? DispatchRowVersion { get; set; }

View file

@ -18,7 +18,6 @@ namespace SeaHaven.Services.Helpers
public const string Pm = "pm";
public const string ServiceNotes = "serviceNotes";
public const string ExtraServices = "extraServices";
public const string FlagColor = "flagColor";
public const string RescheduleCount = "RescheduleCount";
private static readonly HashSet<string> DispatchFields = new(StringComparer.OrdinalIgnoreCase)
@ -45,8 +44,7 @@ namespace SeaHaven.Services.Helpers
DocStatus,
Pm,
ServiceNotes,
ExtraServices,
FlagColor
ExtraServices
};
public static bool IsEditable(string field) => Canonicalize(field) != null;
@ -86,7 +84,6 @@ namespace SeaHaven.Services.Helpers
Pm => "Trade",
ServiceNotes => "ServiceNotes",
ExtraServices => "ExtraServices",
FlagColor => "FlagColor",
_ => canonical
};
}

View file

@ -1,50 +0,0 @@
namespace SeaHaven.Services.Helpers
{
/// <summary>
/// Fixed board flag palette (must match FE FLAG_COLORS). Values stored as canonical #RRGGBB.
/// </summary>
public static class WorkOrderFlagColors
{
public static readonly HashSet<string> Allowed = new(StringComparer.OrdinalIgnoreCase)
{
"#EF4444",
"#F97316",
"#F59E0B",
"#EAB308",
"#22C55E",
"#14B8A6",
"#3B82F6",
"#8B5CF6"
};
/// <summary>
/// Normalizes to # + 6 uppercase hex digits, or null if empty.
/// Returns false if the value is non-empty but not a valid hex color.
/// </summary>
public static bool TryNormalize(string? value, out string? canonical)
{
canonical = null;
if (string.IsNullOrWhiteSpace(value))
return true;
var trimmed = value.Trim();
if (trimmed.Length == 6 && trimmed[0] != '#')
trimmed = "#" + trimmed;
if (trimmed.Length != 7 || trimmed[0] != '#')
return false;
for (var i = 1; i < 7; i++)
{
if (!Char.IsAsciiHexDigit(trimmed[i]))
return false;
}
canonical = "#" + trimmed[1..].ToUpperInvariant();
return true;
}
public static bool IsAllowed(string? canonical)
=> canonical != null && Allowed.Contains(canonical);
}
}

View file

@ -118,7 +118,6 @@ namespace SeaHaven.Services.Implementation
ServiceNotes = row.ServiceNotes,
ExtraServices = ParseExtraServices(row.ExtraServices),
DocStatus = row.DocStatus,
FlagColor = row.FlagColor,
PrimaryDispatchId = row.PrimaryDispatchId,
RowVersion = row.RowVersion,
DispatchRowVersion = row.DispatchRowVersion

View file

@ -161,7 +161,6 @@ namespace SeaHaven.Services.Implementation
WorkOrderBoardFieldNames.Pm => new List<FieldChange> { ApplyStringField(value, auditField, v => workOrder.Trade = v, () => workOrder.Trade) },
WorkOrderBoardFieldNames.ServiceNotes => new List<FieldChange> { ApplyServiceNotes(workOrder, value, auditField) },
WorkOrderBoardFieldNames.ExtraServices => new List<FieldChange> { ApplyExtraServices(workOrder, value, auditField) },
WorkOrderBoardFieldNames.FlagColor => new List<FieldChange> { ApplyFlagColor(workOrder, value, auditField) },
_ => throw new WorkOrderBoardValidationException("InvalidField", $"Field '{field}' is not editable.")
};
}
@ -549,24 +548,6 @@ namespace SeaHaven.Services.Implementation
return FieldChange.ForField(auditField, old, parsed.ToString());
}
private static FieldChange ApplyFlagColor(WorkOrder workOrder, string? value, string auditField)
{
if (!WorkOrderFlagColors.TryNormalize(value, out var canonical))
throw new WorkOrderBoardValidationException("InvalidValue", $"Invalid flagColor: {value}");
if (canonical != null && !WorkOrderFlagColors.IsAllowed(canonical))
throw new WorkOrderBoardValidationException(
"InvalidValue",
$"flagColor must be one of the allowed palette values or empty to clear.");
var old = workOrder.FlagColor;
if (string.Equals(old, canonical, StringComparison.OrdinalIgnoreCase))
return FieldChange.Unchanged(auditField);
workOrder.FlagColor = canonical;
return FieldChange.ForField(auditField, old, canonical);
}
private static List<FieldChange> ApplyAutoScheduleSideEffects(WorkOrder workOrder)
{
var changes = new List<FieldChange>();

View file

@ -114,7 +114,6 @@ namespace SeaHaven.Services.Implementation
ServiceNotes = extended?.ServiceNotes ?? row.ServiceNotes,
ExtraServices = ParseExtraServices(extended?.ExtraServices) ?? row.ExtraServices,
DocStatus = row.DocStatus,
FlagColor = row.FlagColor,
PrimaryDispatchId = row.PrimaryDispatchId,
RowVersion = row.RowVersion,
DispatchRowVersion = row.DispatchRowVersion,

View file

@ -350,157 +350,4 @@ public class WorkOrderBoardUpdateServiceTests
Assert.Equal("InvalidValue", ex.Code);
}
[Fact]
public async Task PatchField_SetsFlagColor_CanonicalUppercase()
{
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.FlagColor,
Value = "#ef4444",
WorkOrderVersion = ToVersion(wo)
}, "actor-1");
Assert.Equal("#EF4444", result.FlagColor);
Assert.Equal("#EF4444", (await context.workOrders.FindAsync(1))!.FlagColor);
}
[Fact]
public async Task PatchField_ClearsFlagColor_WithNullOrEmpty()
{
var (context, service) = CreateSut();
var wo = new WorkOrder
{
Id = 1,
LifecycleStatus = LifecycleStatus.Incomplete,
FlagColor = "#EF4444",
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
};
context.workOrders.Add(wo);
await context.SaveChangesAsync();
var cleared = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
{
Field = WorkOrderBoardFieldNames.FlagColor,
Value = "",
WorkOrderVersion = ToVersion(wo)
}, "actor-1");
Assert.Null(cleared.FlagColor);
Assert.Null((await context.workOrders.FindAsync(1))!.FlagColor);
}
[Fact]
public async Task PatchField_ClearsFlagColor_WithNullValue()
{
var (context, service) = CreateSut();
var wo = new WorkOrder
{
Id = 1,
LifecycleStatus = LifecycleStatus.Incomplete,
FlagColor = "#3B82F6",
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
};
context.workOrders.Add(wo);
await context.SaveChangesAsync();
var cleared = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
{
Field = WorkOrderBoardFieldNames.FlagColor,
Value = null,
WorkOrderVersion = ToVersion(wo)
}, "actor-1");
Assert.Null(cleared.FlagColor);
Assert.Null((await context.workOrders.FindAsync(1))!.FlagColor);
}
[Fact]
public async Task PatchField_FlagColorOutsidePalette_Throws()
{
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<WorkOrderBoardValidationException>(() =>
service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
{
Field = WorkOrderBoardFieldNames.FlagColor,
Value = "#000000",
WorkOrderVersion = ToVersion(wo)
}, "actor-1"));
Assert.Equal("InvalidValue", ex.Code);
}
[Fact]
public async Task PatchField_FlagColorMalformed_Throws()
{
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<WorkOrderBoardValidationException>(() =>
service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
{
Field = WorkOrderBoardFieldNames.FlagColor,
Value = "not-a-color",
WorkOrderVersion = ToVersion(wo)
}, "actor-1"));
Assert.Equal("InvalidValue", ex.Code);
}
[Fact]
public async Task PatchField_FlagColor_DoesNotRequireDispatchVersion()
{
var (context, service) = CreateSut();
var wo = new WorkOrder
{
Id = 1,
LifecycleStatus = LifecycleStatus.Incomplete,
PrimaryDispatchId = 10,
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
};
context.Dispatches.Add(new Dispatch
{
Id = 10,
WorkOrderId = 1,
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 }
});
context.workOrders.Add(wo);
await context.SaveChangesAsync();
var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
{
Field = WorkOrderBoardFieldNames.FlagColor,
Value = "#8B5CF6",
WorkOrderVersion = ToVersion(wo)
// no DispatchVersion
}, "actor-1");
Assert.Equal("#8B5CF6", result.FlagColor);
}
}