mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 16:19:07 +00:00
Merge cc37a0283b into 33574c233b
This commit is contained in:
commit
ef6e790d05
12 changed files with 274 additions and 1 deletions
|
|
@ -0,0 +1,33 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260715180000_WoFlagColor")]
|
||||
public partial class WoFlagColor : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "FlagColor",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(7)",
|
||||
maxLength: 7,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "FlagColor",
|
||||
table: "workOrders");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1975,6 +1975,10 @@ 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)");
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ 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; }
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ namespace SeaHaven.DataServices.Helpers
|
|||
w.ServiceNotes,
|
||||
w.ExtraServices,
|
||||
w.DocStatus,
|
||||
w.FlagColor,
|
||||
w.PrimaryDispatchId,
|
||||
w.RowVersion,
|
||||
DispatchRowVersion = w.PrimaryDispatch != null ? w.PrimaryDispatch.RowVersion : null
|
||||
|
|
@ -115,6 +116,7 @@ namespace SeaHaven.DataServices.Helpers
|
|||
w.ServiceNotes,
|
||||
w.ExtraServices,
|
||||
w.DocStatus,
|
||||
w.FlagColor,
|
||||
w.PrimaryDispatchId,
|
||||
w.RowVersion,
|
||||
w.DispatchRowVersion,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
string? ServiceNotes,
|
||||
string? ExtraServices,
|
||||
DocStatus? DocStatus,
|
||||
string? FlagColor,
|
||||
int? PrimaryDispatchId,
|
||||
byte[]? RowVersion,
|
||||
byte[]? DispatchRowVersion,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ 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; }
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ 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)
|
||||
|
|
@ -44,7 +45,8 @@ namespace SeaHaven.Services.Helpers
|
|||
DocStatus,
|
||||
Pm,
|
||||
ServiceNotes,
|
||||
ExtraServices
|
||||
ExtraServices,
|
||||
FlagColor
|
||||
};
|
||||
|
||||
public static bool IsEditable(string field) => Canonicalize(field) != null;
|
||||
|
|
@ -84,6 +86,7 @@ namespace SeaHaven.Services.Helpers
|
|||
Pm => "Trade",
|
||||
ServiceNotes => "ServiceNotes",
|
||||
ExtraServices => "ExtraServices",
|
||||
FlagColor => "FlagColor",
|
||||
_ => canonical
|
||||
};
|
||||
}
|
||||
|
|
|
|||
50
SeaHaven.Services/Helpers/WorkOrderFlagColors.cs
Normal file
50
SeaHaven.Services/Helpers/WorkOrderFlagColors.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -118,6 +118,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ 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.")
|
||||
};
|
||||
}
|
||||
|
|
@ -548,6 +549,24 @@ 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>();
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ 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,
|
||||
|
|
|
|||
|
|
@ -350,4 +350,157 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue