mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-02 13:13:21 +00:00
Merge pull request #10 from Sea-Haven-Industries/feat/work-orders-domain-foundation
feat(work-orders): Phase 0 domain foundation with FE alignment
This commit is contained in:
commit
32b9113057
34 changed files with 5363 additions and 221 deletions
|
|
@ -21,7 +21,7 @@ using SeaHaven.DataServices.Interfaces;
|
|||
|
||||
namespace Api.SeaHavenIndustries.Controllers
|
||||
{
|
||||
// [Authorize]
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/WorkOrder")]
|
||||
[Route("api/workorders")]
|
||||
|
|
|
|||
22
Api.SeaHavenIndustries/Filters/ConcurrencyExceptionFilter.cs
Normal file
22
Api.SeaHavenIndustries/Filters/ConcurrencyExceptionFilter.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Filters
|
||||
{
|
||||
public class ConcurrencyExceptionFilter : IExceptionFilter
|
||||
{
|
||||
public void OnException(ExceptionContext context)
|
||||
{
|
||||
if (context.Exception is not DbUpdateConcurrencyException) return;
|
||||
|
||||
context.Result = new ConflictObjectResult(new
|
||||
{
|
||||
status = "Conflict",
|
||||
message = "The record was modified by another user. Refresh and retry.",
|
||||
code = 409
|
||||
});
|
||||
context.ExceptionHandled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,10 @@ builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
|
|||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
builder.Services.AddControllers(options =>
|
||||
{
|
||||
options.Filters.Add<Api.SeaHavenIndustries.Filters.ConcurrencyExceptionFilter>();
|
||||
}).AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.MaxDepth = 264; // or any other desired value
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,6 +33,24 @@ namespace Data.SeaHavenIndustries
|
|||
.HasIndex(u => new { u.DispatchId, u.Status })
|
||||
.IsUnique()
|
||||
.HasFilter("[Status] = 'Pending'");
|
||||
|
||||
builder.Entity<WorkOrderFieldLock>()
|
||||
.HasIndex(l => new { l.WorkOrderId, l.FieldName })
|
||||
.IsUnique();
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.Property(w => w.RowVersion)
|
||||
.IsRowVersion();
|
||||
|
||||
builder.Entity<Dispatch>()
|
||||
.Property(d => d.RowVersion)
|
||||
.IsRowVersion();
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.HasOne(w => w.PrimaryDispatch)
|
||||
.WithMany()
|
||||
.HasForeignKey(w => w.PrimaryDispatchId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
public DbSet<Category> Categories { get; set; }
|
||||
public DbSet<Locations> Locations { get; set; }
|
||||
|
|
@ -47,6 +65,7 @@ namespace Data.SeaHavenIndustries
|
|||
public DbSet<WorkOrderCategories> workOrderCategories { get; set; }
|
||||
public DbSet<WorkOrderAttachments> workOrderAttachments { get; set; }
|
||||
public DbSet<WorkOrderAuditLog> WorkOrderAuditLogs { get; set; }
|
||||
public DbSet<WorkOrderFieldLock> WorkOrderFieldLocks { get; set; }
|
||||
public DbSet<DropdownOption> DropdownOptions { get; set; }
|
||||
public DbSet<Vendor> Vendors { get; set; }
|
||||
public DbSet<VendorAccessToken> VendorAccessTokens { get; set; }
|
||||
|
|
@ -88,6 +107,8 @@ namespace Data.SeaHavenIndustries
|
|||
public DateTime? DeletedDate { get; set; }
|
||||
public bool? IsDeleted { get; set; }
|
||||
public string? UserImage { get; set; }
|
||||
public string? Initials { get; set; }
|
||||
public string? Color { get; set; }
|
||||
public int? Type { get; set; } // 1 for users 0 for admin
|
||||
public ICollection<Template>? Templates { get; set; }
|
||||
public ICollection<WorkOrder>? WorkOrders { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,92 +1,171 @@
|
|||
namespace Data.SeaHavenIndustries.Enums
|
||||
{
|
||||
public enum WorkOrderStatus
|
||||
{
|
||||
Open = 1,
|
||||
InProgress = 2,
|
||||
Completed = 3,
|
||||
Cancelled = 4,
|
||||
OnHold = 5
|
||||
}
|
||||
|
||||
public enum WorkOrderPriority
|
||||
{
|
||||
Low = 1,
|
||||
Medium = 2,
|
||||
High = 3,
|
||||
Critical = 4
|
||||
}
|
||||
|
||||
public enum AuditAction
|
||||
{
|
||||
Create = 1,
|
||||
Edit = 2,
|
||||
Delete = 3,
|
||||
StatusChange = 4,
|
||||
AssignmentChange = 5,
|
||||
Dispatch = 6,
|
||||
DispatchUpdate = 7,
|
||||
DispatchVerify = 8
|
||||
}
|
||||
|
||||
public enum ContactType
|
||||
{
|
||||
Primary = 1,
|
||||
Secondary = 2,
|
||||
Billing = 3,
|
||||
Technical = 4
|
||||
}
|
||||
|
||||
public enum AssetStatus
|
||||
{
|
||||
Active = 1,
|
||||
Inactive = 2,
|
||||
UnderMaintenance = 3,
|
||||
Retired = 4
|
||||
}
|
||||
|
||||
public enum DispatchStatus
|
||||
{
|
||||
Pending = 1,
|
||||
Assigned = 2,
|
||||
InProgress = 3,
|
||||
Completed = 4,
|
||||
Cancelled = 5
|
||||
}
|
||||
|
||||
public enum FollowUpStatus
|
||||
{
|
||||
Open = 1,
|
||||
InProgress = 2,
|
||||
Resolved = 3,
|
||||
Closed = 4
|
||||
}
|
||||
|
||||
public enum PMScheduleFrequency
|
||||
{
|
||||
Daily = 1,
|
||||
Weekly = 2,
|
||||
Monthly = 3,
|
||||
Quarterly = 4,
|
||||
SemiAnnual = 5,
|
||||
Annual = 6
|
||||
}
|
||||
|
||||
public enum CommentType
|
||||
{
|
||||
General = 1,
|
||||
Internal = 2,
|
||||
Customer = 3,
|
||||
Vendor = 4,
|
||||
System = 5
|
||||
}
|
||||
|
||||
public enum RecordType
|
||||
{
|
||||
WorkOrder = 1,
|
||||
Dispatch = 2,
|
||||
Quote = 3,
|
||||
Contact = 4,
|
||||
Asset = 5
|
||||
}
|
||||
}
|
||||
namespace Data.SeaHavenIndustries.Enums
|
||||
{
|
||||
/// <summary>Legacy enum — prefer LifecycleStatus for board operations.</summary>
|
||||
public enum WorkOrderStatus
|
||||
{
|
||||
Open = 1,
|
||||
InProgress = 2,
|
||||
Completed = 3,
|
||||
Cancelled = 4,
|
||||
OnHold = 5
|
||||
}
|
||||
|
||||
/// <summary>10 operational lifecycle values aligned with SHOC board (FE labels).</summary>
|
||||
public enum LifecycleStatus
|
||||
{
|
||||
Incomplete = 1,
|
||||
Pending = 2,
|
||||
Scheduled = 3,
|
||||
EnRoute = 4,
|
||||
OnSite = 5,
|
||||
InProgress = 6,
|
||||
Completed = 7,
|
||||
Rescheduled = 8,
|
||||
Canceled = 9,
|
||||
PendingQuote = 10
|
||||
}
|
||||
|
||||
/// <summary>Board work-order types. Legacy values retained for backward compatibility.</summary>
|
||||
public enum WorkOrderType
|
||||
{
|
||||
PO = 1,
|
||||
PM = 2,
|
||||
Emergency = 3,
|
||||
Project = 4,
|
||||
Inspection = 5,
|
||||
Reactive = 6,
|
||||
AddOn = 7,
|
||||
Other = 99
|
||||
}
|
||||
|
||||
public enum DocStatus
|
||||
{
|
||||
Yes = 1,
|
||||
No = 2,
|
||||
NotNeeded = 3
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum OperationalFlags
|
||||
{
|
||||
None = 0,
|
||||
PastDue = 1
|
||||
}
|
||||
|
||||
public enum AuditEventType
|
||||
{
|
||||
Manual = 1,
|
||||
System = 2,
|
||||
Sync = 3,
|
||||
Vendor = 4
|
||||
}
|
||||
|
||||
public enum AuditActorType
|
||||
{
|
||||
Dispatcher = 1,
|
||||
Vendor = 2,
|
||||
System = 3,
|
||||
Sync = 4
|
||||
}
|
||||
|
||||
public enum AuditActionType
|
||||
{
|
||||
FieldChanged = 1,
|
||||
StatusChanged = 2,
|
||||
AssignmentChanged = 3,
|
||||
WeekRolled = 4,
|
||||
SyncRejected = 5,
|
||||
Create = 6,
|
||||
Delete = 7
|
||||
}
|
||||
|
||||
public enum WorkOrderPriority
|
||||
{
|
||||
Low = 1,
|
||||
Medium = 2,
|
||||
High = 3,
|
||||
Critical = 4
|
||||
}
|
||||
|
||||
public enum AuditAction
|
||||
{
|
||||
Create = 1,
|
||||
Edit = 2,
|
||||
Delete = 3,
|
||||
StatusChange = 4,
|
||||
AssignmentChange = 5,
|
||||
Dispatch = 6,
|
||||
DispatchUpdate = 7,
|
||||
DispatchVerify = 8
|
||||
}
|
||||
|
||||
public enum ContactType
|
||||
{
|
||||
Primary = 1,
|
||||
Secondary = 2,
|
||||
Billing = 3,
|
||||
Technical = 4
|
||||
}
|
||||
|
||||
public enum AssetStatus
|
||||
{
|
||||
Active = 1,
|
||||
Inactive = 2,
|
||||
UnderMaintenance = 3,
|
||||
Retired = 4
|
||||
}
|
||||
|
||||
public enum DispatchStatus
|
||||
{
|
||||
Pending = 1,
|
||||
Assigned = 2,
|
||||
InProgress = 3,
|
||||
Completed = 4,
|
||||
Cancelled = 5
|
||||
}
|
||||
|
||||
public enum FollowUpStatus
|
||||
{
|
||||
Open = 1,
|
||||
InProgress = 2,
|
||||
Resolved = 3,
|
||||
Closed = 4
|
||||
}
|
||||
|
||||
public enum PMScheduleFrequency
|
||||
{
|
||||
Daily = 1,
|
||||
Weekly = 2,
|
||||
Monthly = 3,
|
||||
Quarterly = 4,
|
||||
SemiAnnual = 5,
|
||||
Annual = 6
|
||||
}
|
||||
|
||||
public enum CommentType
|
||||
{
|
||||
General = 1,
|
||||
Internal = 2,
|
||||
Customer = 3,
|
||||
Vendor = 4,
|
||||
System = 5
|
||||
}
|
||||
|
||||
public enum RecordType
|
||||
{
|
||||
WorkOrder = 1,
|
||||
Dispatch = 2,
|
||||
Quote = 3,
|
||||
Contact = 4,
|
||||
Asset = 5
|
||||
}
|
||||
|
||||
public enum WorkOrderMediaCategory
|
||||
{
|
||||
Before = 1,
|
||||
After = 2,
|
||||
Extra = 3,
|
||||
Completion = 4
|
||||
}
|
||||
}
|
||||
|
||||
2991
Data.SeaHavenIndustries/Migrations/20260624163145_Phase0_DomainFoundation.Designer.cs
generated
Normal file
2991
Data.SeaHavenIndustries/Migrations/20260624163145_Phase0_DomainFoundation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,744 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Phase0_DomainFoundation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Dispatches_workOrders_WorkOrderId",
|
||||
table: "Dispatches");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Locations_Accounts_AccountId",
|
||||
table: "Locations");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Locations_AccountId",
|
||||
table: "Locations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedDate",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeleterUserId",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletionTime",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModificationTime",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModifierUserId",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "createdby",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AccountId",
|
||||
table: "Locations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedDate",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeleterUserId",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletionTime",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModificationTime",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModifierUserId",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "createdby",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedDate",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeleterUserId",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletionTime",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModificationTime",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModifierUserId",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "createdby",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedDate",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeleterUserId",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletionTime",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModificationTime",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModifierUserId",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "createdby",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "ZipCode",
|
||||
table: "Locations",
|
||||
newName: "Zip");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Phone",
|
||||
table: "Locations",
|
||||
newName: "PhoneNumber");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "ContactEmail",
|
||||
table: "Locations",
|
||||
newName: "Email");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Contact",
|
||||
table: "Locations",
|
||||
newName: "Address2");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Address",
|
||||
table: "Locations",
|
||||
newName: "Address1");
|
||||
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
name: "AssignDate",
|
||||
table: "workOrders",
|
||||
type: "date",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AvettaTask",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "CarriedOver",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DocStatus",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "LegacyStatus",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "LifecycleStatus",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "OperationalFlags",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
name: "OriginalDate",
|
||||
table: "workOrders",
|
||||
type: "date",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
name: "OriginalWeek",
|
||||
table: "workOrders",
|
||||
type: "date",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "PrimaryDispatchId",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "RescheduleCount",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "workOrders",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ScheduleWeekOnly",
|
||||
table: "workOrders",
|
||||
type: "bit",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ScheduledEnd",
|
||||
table: "workOrders",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
name: "TargetWeek",
|
||||
table: "workOrders",
|
||||
type: "date",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "WorkOrderType",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Service",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(128)",
|
||||
maxLength: 128,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Notes",
|
||||
table: "WorkOrderContacts",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ActorType",
|
||||
table: "WorkOrderAuditLogs",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CorrelationId",
|
||||
table: "WorkOrderAuditLogs",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DispatchId",
|
||||
table: "WorkOrderAuditLogs",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "EventType",
|
||||
table: "WorkOrderAuditLogs",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "Dispatches",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Color",
|
||||
table: "AspNetUsers",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Initials",
|
||||
table: "AspNetUsers",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WorkOrderFieldLocks",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
WorkOrderId = table.Column<int>(type: "int", nullable: false),
|
||||
FieldName = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
LockedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
LockedByUserId = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WorkOrderFieldLocks", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_WorkOrderFieldLocks_workOrders_WorkOrderId",
|
||||
column: x => x.WorkOrderId,
|
||||
principalTable: "workOrders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_workOrders_PrimaryDispatchId",
|
||||
table: "workOrders",
|
||||
column: "PrimaryDispatchId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WorkOrderFieldLocks_WorkOrderId_FieldName",
|
||||
table: "WorkOrderFieldLocks",
|
||||
columns: new[] { "WorkOrderId", "FieldName" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Dispatches_workOrders_WorkOrderId",
|
||||
table: "Dispatches",
|
||||
column: "WorkOrderId",
|
||||
principalTable: "workOrders",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_workOrders_Dispatches_PrimaryDispatchId",
|
||||
table: "workOrders",
|
||||
column: "PrimaryDispatchId",
|
||||
principalTable: "Dispatches",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE workOrders SET LegacyStatus = Status WHERE LegacyStatus IS NULL AND Status IS NOT NULL;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE workOrders SET LifecycleStatus = CASE
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('open', 'incomplete') THEN 1
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('pending', 'onhold', 'on hold') THEN 2
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('scheduled') THEN 3
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('en route', 'enroute') THEN 4
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('on site', 'onsite') THEN 5
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('inprogress', 'in progress') THEN 6
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('completed', 'complete', 'done', 'closed') THEN 7
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('rescheduled') THEN 8
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('cancelled', 'canceled') THEN 9
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('pending quote', 'pendingquote', 'pendingapproval', 'pending approval') THEN 10
|
||||
ELSE 1
|
||||
END
|
||||
WHERE LifecycleStatus IS NULL AND Status IS NOT NULL;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE wo SET wo.PrimaryDispatchId = latest.DispatchId
|
||||
FROM workOrders wo
|
||||
INNER JOIN (
|
||||
SELECT WorkOrderId, MAX(Id) AS DispatchId
|
||||
FROM Dispatches
|
||||
WHERE WorkOrderId IS NOT NULL
|
||||
GROUP BY WorkOrderId
|
||||
) latest ON wo.Id = latest.WorkOrderId
|
||||
WHERE wo.PrimaryDispatchId IS NULL;
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Dispatches_workOrders_WorkOrderId",
|
||||
table: "Dispatches");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_workOrders_Dispatches_PrimaryDispatchId",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "WorkOrderFieldLocks");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_workOrders_PrimaryDispatchId",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AssignDate",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AvettaTask",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CarriedOver",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DocStatus",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LegacyStatus",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LifecycleStatus",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OperationalFlags",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OriginalDate",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OriginalWeek",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PrimaryDispatchId",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RescheduleCount",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ScheduleWeekOnly",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ScheduledEnd",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TargetWeek",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "WorkOrderType",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Service",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Notes",
|
||||
table: "WorkOrderContacts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ActorType",
|
||||
table: "WorkOrderAuditLogs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CorrelationId",
|
||||
table: "WorkOrderAuditLogs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DispatchId",
|
||||
table: "WorkOrderAuditLogs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EventType",
|
||||
table: "WorkOrderAuditLogs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "Dispatches");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Color",
|
||||
table: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Initials",
|
||||
table: "AspNetUsers");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Zip",
|
||||
table: "Locations",
|
||||
newName: "ZipCode");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "PhoneNumber",
|
||||
table: "Locations",
|
||||
newName: "Phone");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Email",
|
||||
table: "Locations",
|
||||
newName: "ContactEmail");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Address2",
|
||||
table: "Locations",
|
||||
newName: "Contact");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Address1",
|
||||
table: "Locations",
|
||||
newName: "Address");
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedDate",
|
||||
table: "Regions",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DeleterUserId",
|
||||
table: "Regions",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "DeletionTime",
|
||||
table: "Regions",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
table: "Regions",
|
||||
type: "bit",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastModificationTime",
|
||||
table: "Regions",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "LastModifierUserId",
|
||||
table: "Regions",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "createdby",
|
||||
table: "Regions",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AccountId",
|
||||
table: "Locations",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedDate",
|
||||
table: "JobTitles",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DeleterUserId",
|
||||
table: "JobTitles",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "DeletionTime",
|
||||
table: "JobTitles",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
table: "JobTitles",
|
||||
type: "bit",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastModificationTime",
|
||||
table: "JobTitles",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "LastModifierUserId",
|
||||
table: "JobTitles",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "createdby",
|
||||
table: "JobTitles",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedDate",
|
||||
table: "EmployeeEmails",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DeleterUserId",
|
||||
table: "EmployeeEmails",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "DeletionTime",
|
||||
table: "EmployeeEmails",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
table: "EmployeeEmails",
|
||||
type: "bit",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastModificationTime",
|
||||
table: "EmployeeEmails",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "LastModifierUserId",
|
||||
table: "EmployeeEmails",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "createdby",
|
||||
table: "EmployeeEmails",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedDate",
|
||||
table: "Departments",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DeleterUserId",
|
||||
table: "Departments",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "DeletionTime",
|
||||
table: "Departments",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
table: "Departments",
|
||||
type: "bit",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastModificationTime",
|
||||
table: "Departments",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "LastModifierUserId",
|
||||
table: "Departments",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "createdby",
|
||||
table: "Departments",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Locations_AccountId",
|
||||
table: "Locations",
|
||||
column: "AccountId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Dispatches_workOrders_WorkOrderId",
|
||||
table: "Dispatches",
|
||||
column: "WorkOrderId",
|
||||
principalTable: "workOrders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Locations_Accounts_AccountId",
|
||||
table: "Locations",
|
||||
column: "AccountId",
|
||||
principalTable: "Accounts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -186,6 +186,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Color")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
|
@ -209,6 +212,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("FirstName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Initials")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
|
|
@ -601,32 +607,11 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DeleterUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Departments");
|
||||
|
|
@ -688,6 +673,11 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("ReplyToAddress")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<DateTime?>("ScheduledDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -1111,36 +1101,15 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DeleterUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("EmailTypeId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("EmployeeId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool?>("IsPrimary")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmployeeId");
|
||||
|
|
@ -1279,32 +1248,11 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DeleterUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobTitles");
|
||||
|
|
@ -1318,21 +1266,15 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("AccountId")
|
||||
.HasColumnType("int");
|
||||
b.Property<string>("Address1")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Address")
|
||||
b.Property<string>("Address2")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("City")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Contact")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ContactEmail")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -1342,6 +1284,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
|
|
@ -1360,7 +1305,7 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("State")
|
||||
|
|
@ -1372,7 +1317,7 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Title")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ZipCode")
|
||||
b.Property<string>("Zip")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
|
|
@ -1380,8 +1325,6 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Locations");
|
||||
});
|
||||
|
||||
|
|
@ -1573,32 +1516,11 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DeleterUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Regions");
|
||||
|
|
@ -1931,12 +1853,18 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("AfterPhotoIssue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateOnly?>("AssignDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("AssignTo")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Attachments")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("AvettaTask")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("BeforPhotoAttachment")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
@ -1946,6 +1874,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Building")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("CarriedOver")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("CompletedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -1967,6 +1898,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("DocStatus")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("DueDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -1985,27 +1919,63 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("LegacyStatus")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("LifecycleStatus")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("LocationId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("OperationalFlags")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateOnly?>("OriginalDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly?>("OriginalWeek")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("PO")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("PrimaryDispatchId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Problem")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("RescheduleCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<bool?>("ScheduleWeekOnly")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("ScheduledDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ScheduledEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ScheduledStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Service")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("SignOffAttachment")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
@ -2033,12 +2003,18 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("TT")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateOnly?>("TargetWeek")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("Trade")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<decimal?>("VendorNTE")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<int?>("WorkOrderType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("WorkerOrderNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
@ -2057,6 +2033,8 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
b.HasIndex("LocationId");
|
||||
|
||||
b.HasIndex("PrimaryDispatchId");
|
||||
|
||||
b.ToTable("workOrders");
|
||||
});
|
||||
|
||||
|
|
@ -2113,9 +2091,21 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Action")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ActorType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("CorrelationId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("DispatchId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("FieldName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
@ -2213,6 +2203,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("WorkorderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
|
|
@ -2228,6 +2221,35 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.ToTable("WorkOrderContacts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderFieldLock", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("FieldName")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTime>("LockedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LockedByUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("WorkOrderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkOrderId", "FieldName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("WorkOrderFieldLocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
|
|
@ -2457,8 +2479,7 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder")
|
||||
.WithMany("Dispatches")
|
||||
.HasForeignKey("WorkOrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
.HasForeignKey("WorkOrderId");
|
||||
|
||||
b.Navigation("Vendor");
|
||||
|
||||
|
|
@ -2612,16 +2633,6 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Navigation("WorkOrder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.Locations", b =>
|
||||
{
|
||||
b.HasOne("Data.SeaHavenIndustries.Accounts", "Account")
|
||||
.WithMany()
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.PMSchedules", b =>
|
||||
{
|
||||
b.HasOne("Data.SeaHavenIndustries.Assets", "Asset")
|
||||
|
|
@ -2731,9 +2742,16 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
.HasForeignKey("LocationId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("Data.SeaHavenIndustries.Dispatch", "PrimaryDispatch")
|
||||
.WithMany()
|
||||
.HasForeignKey("PrimaryDispatchId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("AssignToUser");
|
||||
|
||||
b.Navigation("Locations");
|
||||
|
||||
b.Navigation("PrimaryDispatch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderAttachments", b =>
|
||||
|
|
@ -2798,6 +2816,17 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Navigation("WorkOrder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderFieldLock", b =>
|
||||
{
|
||||
b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder")
|
||||
.WithMany("FieldLocks")
|
||||
.HasForeignKey("WorkOrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("WorkOrder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
|
|
@ -2943,6 +2972,8 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
b.Navigation("Dispatches");
|
||||
|
||||
b.Navigation("FieldLocks");
|
||||
|
||||
b.Navigation("Quotes");
|
||||
|
||||
b.Navigation("WorkOrderCategories");
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Data.SeaHavenIndustries
|
||||
|
|
@ -26,6 +27,10 @@ namespace Data.SeaHavenIndustries
|
|||
public string? AcceptToken { get; set; }
|
||||
public string? VerifiedBy { get; set; }
|
||||
public DateTime? VerifiedAt { get; set; }
|
||||
|
||||
[Timestamp]
|
||||
public byte[]? RowVersion { get; set; }
|
||||
|
||||
public ICollection<Comments>? Comments { get; set; }
|
||||
public ICollection<DispatchWorkOrder>? DispatchWorkOrders { get; set; }
|
||||
public ICollection<DispatchChecklistItem>? ChecklistItems { get; set; }
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ namespace Data.SeaHavenIndustries
|
|||
public string? OldValue { get; set; }
|
||||
public string? NewValue { get; set; }
|
||||
public string? Action { get; set; }
|
||||
public string? EventType { get; set; }
|
||||
public string? ActorType { get; set; }
|
||||
public int? DispatchId { get; set; }
|
||||
public string? CorrelationId { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@ namespace Data.SeaHavenIndustries
|
|||
public int? WorkorderId { get; set; }
|
||||
[ForeignKey(nameof(WorkorderId))]
|
||||
public virtual WorkOrder? WorkOrder { get; set; }
|
||||
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
Data.SeaHavenIndustries/Models/WorkOrderFieldLock.cs
Normal file
18
Data.SeaHavenIndustries/Models/WorkOrderFieldLock.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Data.SeaHavenIndustries
|
||||
{
|
||||
/// <summary>
|
||||
/// ManualEditFlag — blocks Sync overwrite after first SHOC edit per field.
|
||||
/// </summary>
|
||||
public class WorkOrderFieldLock
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int WorkOrderId { get; set; }
|
||||
[ForeignKey(nameof(WorkOrderId))]
|
||||
public virtual WorkOrder? WorkOrder { get; set; }
|
||||
public string FieldName { get; set; } = "";
|
||||
public DateTime LockedAt { get; set; }
|
||||
public string? LockedByUserId { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace Data.SeaHavenIndustries
|
||||
{
|
||||
public class WorkOrder : FullAuditEntity
|
||||
{
|
||||
// --- Core slice ---
|
||||
public string? InternalWONumber { get; set; }
|
||||
public string? ExternalWorkOrderId { get; set; }
|
||||
public string? WorkerOrderNumber { get; set; }
|
||||
|
|
@ -15,34 +16,59 @@ namespace Data.SeaHavenIndustries
|
|||
public string? SiteCode { get; set; }
|
||||
public string? Building { get; set; }
|
||||
public string? Severity { get; set; }
|
||||
public DateTime? DateReported { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
public string? AssignTo { get; set; }
|
||||
[ForeignKey(nameof(AssignTo))]
|
||||
public virtual ApplicationUser? AssignToUser { get; set; }
|
||||
public int? LocationId { get; set; }
|
||||
[ForeignKey(nameof(LocationId))]
|
||||
public virtual Locations? Locations { get; set; }
|
||||
public LifecycleStatus? LifecycleStatus { get; set; }
|
||||
public string? LegacyStatus { get; set; }
|
||||
public WorkOrderType? WorkOrderType { get; set; }
|
||||
/// <summary>SHOC board "Service" column (PM type label). Not Trade/Problem.</summary>
|
||||
public string? Service { get; set; }
|
||||
public int? PrimaryDispatchId { get; set; }
|
||||
[ForeignKey(nameof(PrimaryDispatchId))]
|
||||
public virtual Dispatch? PrimaryDispatch { get; set; }
|
||||
|
||||
// --- Scheduling slice ---
|
||||
public DateTime? ScheduledDate { get; set; }
|
||||
public DateTime? ScheduledStart { get; set; }
|
||||
public DateTime? ScheduledEnd { get; set; }
|
||||
public DateOnly? TargetWeek { get; set; }
|
||||
public bool? ScheduleWeekOnly { get; set; }
|
||||
|
||||
// --- Tracking slice (set-once) ---
|
||||
public DateOnly? OriginalWeek { get; set; }
|
||||
public DateOnly? OriginalDate { get; set; }
|
||||
|
||||
// --- Analytics slice ---
|
||||
public int RescheduleCount { get; set; }
|
||||
public int CarriedOver { get; set; }
|
||||
public OperationalFlags OperationalFlags { get; set; }
|
||||
|
||||
// --- Completion slice ---
|
||||
public DocStatus? DocStatus { get; set; }
|
||||
|
||||
// --- Legacy SQL columns (drift reconciliation) ---
|
||||
public string? AvettaTask { get; set; }
|
||||
public DateOnly? AssignDate { get; set; }
|
||||
|
||||
// --- Legacy fields (retained for Blazor / coexistence) ---
|
||||
public DateTime? DateReported { get; set; }
|
||||
public string? SourceEmailS3Key { get; set; }
|
||||
public string? Problem { get; set; }
|
||||
public string? Trade { get; set; }
|
||||
public string? SubTrade { get; set; }
|
||||
public decimal? VendorNTE { get; set; }
|
||||
public DateTime? ScheduledDate { get; set; }
|
||||
public DateTime? CompletedDate { get; set; }
|
||||
public string? Source { get; set; }
|
||||
public bool? istemplate { get; set; }
|
||||
public string? PO { get; set; }
|
||||
public string? TT { get; set; }
|
||||
//public int? ContactId { get; set; }
|
||||
//[ForeignKey(nameof(ContactId))]
|
||||
//public virtual Contacts? POC { get; set; }
|
||||
public string? AssignTo { get; set; }
|
||||
[ForeignKey(nameof(AssignTo))]
|
||||
public virtual ApplicationUser? AssignToUser { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
public string? Priority { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public int? LocationId { get; set; }
|
||||
[ForeignKey(nameof(LocationId))]
|
||||
public virtual Locations? Locations { get; set; }
|
||||
//public int? CategoryId { get; set; }
|
||||
//[ForeignKey(nameof(CategoryId))]
|
||||
//public virtual Category? Category { get; set; }
|
||||
public string? Attachments { get; set; }
|
||||
public string? BeforPhotoIssue { get; set; }
|
||||
public string? BeforPhotoAttachment { get; set; }
|
||||
|
|
@ -51,11 +77,16 @@ namespace Data.SeaHavenIndustries
|
|||
public string? SignOffName { get; set; }
|
||||
public string? SignOffAttachment { get; set; }
|
||||
public string? SignOffSignature { get; set; }
|
||||
|
||||
[Timestamp]
|
||||
public byte[]? RowVersion { get; set; }
|
||||
|
||||
public ICollection<Comments>? Comments { get; set; }
|
||||
public ICollection<Quotes>? Quotes { get; set; }
|
||||
public ICollection<WorkOrderContacts>? WorkOrderContacts { get; set; }
|
||||
public ICollection<WorkOrderAttachments>? workOrderAttachments { get; set; }
|
||||
public ICollection<WorkOrderCategories>? WorkOrderCategories { get; set; }
|
||||
public ICollection<Dispatch>? Dispatches { get; set; }
|
||||
public ICollection<WorkOrderFieldLock>? FieldLocks { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
SeaHaven.Services/Constants/WorkOrderFieldNames.cs
Normal file
26
SeaHaven.Services/Constants/WorkOrderFieldNames.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
namespace SeaHaven.Services.Constants
|
||||
{
|
||||
public static class WorkOrderFieldNames
|
||||
{
|
||||
public const string LifecycleStatus = nameof(LifecycleStatus);
|
||||
public const string AssignTo = nameof(AssignTo);
|
||||
public const string ScheduledDate = nameof(ScheduledDate);
|
||||
public const string ScheduledEnd = nameof(ScheduledEnd);
|
||||
public const string TargetWeek = nameof(TargetWeek);
|
||||
public const string ScheduleWeekOnly = nameof(ScheduleWeekOnly);
|
||||
public const string DueDate = nameof(DueDate);
|
||||
public const string Description = nameof(Description);
|
||||
public const string WorkerOrderTitle = nameof(WorkerOrderTitle);
|
||||
public const string SiteCode = nameof(SiteCode);
|
||||
public const string Building = nameof(Building);
|
||||
public const string LocationId = nameof(LocationId);
|
||||
public const string WorkOrderType = nameof(WorkOrderType);
|
||||
public const string Service = nameof(Service);
|
||||
public const string DocStatus = nameof(DocStatus);
|
||||
public const string Trade = nameof(Trade);
|
||||
public const string Problem = nameof(Problem);
|
||||
public const string Priority = nameof(Priority);
|
||||
public const string InternalWONumber = nameof(InternalWONumber);
|
||||
public const string Status = nameof(Status);
|
||||
}
|
||||
}
|
||||
86
SeaHaven.Services/Helpers/LifecycleStatusMapper.cs
Normal file
86
SeaHaven.Services/Helpers/LifecycleStatusMapper.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class LifecycleStatusMapper
|
||||
{
|
||||
public static LifecycleStatus? FromLegacyStatus(string? status)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(status)) return null;
|
||||
|
||||
return status.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"open" or "incomplete" => LifecycleStatus.Incomplete,
|
||||
"pending" or "onhold" or "on hold" => LifecycleStatus.Pending,
|
||||
"scheduled" => LifecycleStatus.Scheduled,
|
||||
"en route" or "enroute" => LifecycleStatus.EnRoute,
|
||||
"on site" or "onsite" => LifecycleStatus.OnSite,
|
||||
"inprogress" or "in progress" => LifecycleStatus.InProgress,
|
||||
"completed" or "complete" or "done" or "closed" => LifecycleStatus.Completed,
|
||||
"rescheduled" => LifecycleStatus.Rescheduled,
|
||||
"cancelled" or "canceled" => LifecycleStatus.Canceled,
|
||||
"pending quote" or "pendingquote" or "pendingapproval" or "pending approval" => LifecycleStatus.PendingQuote,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public static LifecycleStatus? ParseLifecycleStatus(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
|
||||
if (TryParseFeLabel(value, out var fromLabel))
|
||||
return fromLabel;
|
||||
|
||||
if (Enum.TryParse<LifecycleStatus>(value, true, out var parsed))
|
||||
return parsed;
|
||||
|
||||
return FromLegacyStatus(value);
|
||||
}
|
||||
|
||||
public static bool TryParseFeLabel(string? label, out LifecycleStatus status)
|
||||
{
|
||||
status = default;
|
||||
if (string.IsNullOrWhiteSpace(label)) return false;
|
||||
|
||||
return label.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"incomplete" => Set(LifecycleStatus.Incomplete, out status),
|
||||
"pending" => Set(LifecycleStatus.Pending, out status),
|
||||
"scheduled" => Set(LifecycleStatus.Scheduled, out status),
|
||||
"en route" => Set(LifecycleStatus.EnRoute, out status),
|
||||
"on site" => Set(LifecycleStatus.OnSite, out status),
|
||||
"in progress" => Set(LifecycleStatus.InProgress, out status),
|
||||
"completed" => Set(LifecycleStatus.Completed, out status),
|
||||
"rescheduled" => Set(LifecycleStatus.Rescheduled, out status),
|
||||
"canceled" => Set(LifecycleStatus.Canceled, out status),
|
||||
"pending quote" => Set(LifecycleStatus.PendingQuote, out status),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToFeLabel(LifecycleStatus status) => status switch
|
||||
{
|
||||
LifecycleStatus.Incomplete => "Incomplete",
|
||||
LifecycleStatus.Pending => "Pending",
|
||||
LifecycleStatus.Scheduled => "Scheduled",
|
||||
LifecycleStatus.EnRoute => "En Route",
|
||||
LifecycleStatus.OnSite => "On Site",
|
||||
LifecycleStatus.InProgress => "In Progress",
|
||||
LifecycleStatus.Completed => "Completed",
|
||||
LifecycleStatus.Rescheduled => "Rescheduled",
|
||||
LifecycleStatus.Canceled => "Canceled",
|
||||
LifecycleStatus.PendingQuote => "Pending Quote",
|
||||
_ => status.ToString()
|
||||
};
|
||||
|
||||
public static bool IsTerminal(LifecycleStatus? status)
|
||||
=> status is LifecycleStatus.Completed or LifecycleStatus.Canceled;
|
||||
|
||||
private static bool Set(LifecycleStatus value, out LifecycleStatus status)
|
||||
{
|
||||
status = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
55
SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs
Normal file
55
SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderDerivedFields
|
||||
{
|
||||
/// <summary>
|
||||
/// Past due overlay — never a WorkOrderType. Matches SHOC board design.
|
||||
/// </summary>
|
||||
public static bool IsPastDue(WorkOrder workOrder, DateOnly? asOf = null)
|
||||
{
|
||||
var today = asOf ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
if (workOrder.ScheduledDate == null)
|
||||
return false;
|
||||
|
||||
if (LifecycleStatusMapper.IsTerminal(workOrder.LifecycleStatus))
|
||||
return false;
|
||||
|
||||
return DateOnly.FromDateTime(workOrder.ScheduledDate.Value.Date) < today;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets initial lifecycle on create. New WOs start Incomplete unless auto-scheduled.
|
||||
/// </summary>
|
||||
public static void SetInitialLifecycleStatus(WorkOrder workOrder)
|
||||
{
|
||||
workOrder.LifecycleStatus ??= LifecycleStatus.Incomplete;
|
||||
ApplyAutoScheduleIfEligible(workOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHOC rule: specific scheduled date + assignee + Incomplete → Scheduled.
|
||||
/// Week-only targets do not auto-schedule.
|
||||
/// </summary>
|
||||
public static bool ApplyAutoScheduleIfEligible(WorkOrder workOrder)
|
||||
{
|
||||
if (workOrder.LifecycleStatus != LifecycleStatus.Incomplete)
|
||||
return false;
|
||||
|
||||
if (workOrder.ScheduleWeekOnly == true)
|
||||
return false;
|
||||
|
||||
if (workOrder.ScheduledDate == null)
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(workOrder.AssignTo))
|
||||
return false;
|
||||
|
||||
workOrder.LifecycleStatus = LifecycleStatus.Scheduled;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
SeaHaven.Services/Helpers/WorkOrderNumberNormalizer.cs
Normal file
37
SeaHaven.Services/Helpers/WorkOrderNumberNormalizer.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderNumberNormalizer
|
||||
{
|
||||
private const int RequiredLength = 11;
|
||||
|
||||
public static bool TryNormalize(string? input, out string normalized, out string? error)
|
||||
{
|
||||
normalized = "";
|
||||
error = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
error = "WO number is required.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var digits = Regex.Replace(input.Trim(), @"\D", "");
|
||||
if (digits.Length == 0)
|
||||
{
|
||||
error = "WO number must contain digits.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (digits.Length > RequiredLength)
|
||||
{
|
||||
error = $"WO number cannot exceed {RequiredLength} digits.";
|
||||
return false;
|
||||
}
|
||||
|
||||
normalized = digits.PadLeft(RequiredLength, '0');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
SeaHaven.Services/Helpers/WorkOrderTypeMapper.cs
Normal file
48
SeaHaven.Services/Helpers/WorkOrderTypeMapper.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderTypeMapper
|
||||
{
|
||||
/// <summary>Board FE types. Overdue is not a type — use <see cref="WorkOrderDerivedFields.IsPastDue"/>.</summary>
|
||||
public static bool TryParseFeLabel(string? label, out WorkOrderType type)
|
||||
{
|
||||
type = default;
|
||||
if (string.IsNullOrWhiteSpace(label)) return false;
|
||||
|
||||
return label.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"pm" => Set(WorkOrderType.PM, out type),
|
||||
"reactive" => Set(WorkOrderType.Reactive, out type),
|
||||
"emergency" => Set(WorkOrderType.Emergency, out type),
|
||||
"add-on" or "addon" => Set(WorkOrderType.AddOn, out type),
|
||||
"po" => Set(WorkOrderType.PO, out type),
|
||||
"project" => Set(WorkOrderType.Project, out type),
|
||||
"inspection" => Set(WorkOrderType.Inspection, out type),
|
||||
"other" => Set(WorkOrderType.Other, out type),
|
||||
"overdue" => false,
|
||||
_ => Enum.TryParse(label, true, out type)
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToFeLabel(WorkOrderType type) => type switch
|
||||
{
|
||||
WorkOrderType.PM => "PM",
|
||||
WorkOrderType.Reactive => "Reactive",
|
||||
WorkOrderType.Emergency => "Emergency",
|
||||
WorkOrderType.AddOn => "Add-On",
|
||||
WorkOrderType.PO => "PO",
|
||||
WorkOrderType.Project => "Project",
|
||||
WorkOrderType.Inspection => "Inspection",
|
||||
WorkOrderType.Other => "Other",
|
||||
_ => type.ToString()
|
||||
};
|
||||
|
||||
private static bool Set(WorkOrderType value, out WorkOrderType type)
|
||||
{
|
||||
type = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
219
SeaHaven.Services/Implementation/SyncFieldMergePolicy.cs
Normal file
219
SeaHaven.Services/Implementation/SyncFieldMergePolicy.cs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.Constants;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
public class SyncFieldMergePolicy : ISyncFieldMergePolicy
|
||||
{
|
||||
private static readonly HashSet<string> ShocOwnedFieldSet = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
WorkOrderFieldNames.LifecycleStatus,
|
||||
WorkOrderFieldNames.AssignTo,
|
||||
WorkOrderFieldNames.ScheduledDate,
|
||||
WorkOrderFieldNames.ScheduledEnd,
|
||||
WorkOrderFieldNames.TargetWeek,
|
||||
WorkOrderFieldNames.ScheduleWeekOnly,
|
||||
WorkOrderFieldNames.DueDate,
|
||||
WorkOrderFieldNames.Description,
|
||||
WorkOrderFieldNames.WorkerOrderTitle,
|
||||
WorkOrderFieldNames.SiteCode,
|
||||
WorkOrderFieldNames.Building,
|
||||
WorkOrderFieldNames.LocationId,
|
||||
WorkOrderFieldNames.WorkOrderType,
|
||||
WorkOrderFieldNames.Service,
|
||||
WorkOrderFieldNames.DocStatus,
|
||||
WorkOrderFieldNames.Trade,
|
||||
WorkOrderFieldNames.Problem,
|
||||
WorkOrderFieldNames.Priority,
|
||||
WorkOrderFieldNames.InternalWONumber,
|
||||
WorkOrderFieldNames.Status,
|
||||
};
|
||||
|
||||
private readonly IWorkOrderFieldLockService _fieldLocks;
|
||||
|
||||
public SyncFieldMergePolicy(IWorkOrderFieldLockService fieldLocks)
|
||||
{
|
||||
_fieldLocks = fieldLocks;
|
||||
}
|
||||
|
||||
public IReadOnlySet<string> ShocOwnedFields => ShocOwnedFieldSet;
|
||||
|
||||
public async Task<bool> CanSyncOverwriteAsync(int workOrderId, string fieldName)
|
||||
=> !await _fieldLocks.IsLockedAsync(workOrderId, fieldName);
|
||||
|
||||
public async Task<bool> TryApplyAsync(WorkOrderSyncContext context, string fieldName, object? incomingValue)
|
||||
{
|
||||
if (incomingValue == null) return false;
|
||||
|
||||
if (await context.FieldLocks.IsLockedAsync(context.WorkOrder.Id, fieldName))
|
||||
{
|
||||
var current = GetFieldValue(context.WorkOrder, fieldName);
|
||||
await context.Audit.LogSyncRejectedAsync(
|
||||
context.WorkOrder.Id,
|
||||
fieldName,
|
||||
current,
|
||||
incomingValue.ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryApplyField(context.WorkOrder, fieldName, incomingValue);
|
||||
}
|
||||
|
||||
private static string? GetFieldValue(WorkOrder wo, string fieldName) => fieldName switch
|
||||
{
|
||||
WorkOrderFieldNames.Description => wo.Description,
|
||||
WorkOrderFieldNames.WorkerOrderTitle => wo.WorkerOrderTitle,
|
||||
WorkOrderFieldNames.Status => wo.Status,
|
||||
WorkOrderFieldNames.DueDate => wo.DueDate?.ToString("O"),
|
||||
WorkOrderFieldNames.ScheduledDate => wo.ScheduledDate?.ToString("O"),
|
||||
WorkOrderFieldNames.ScheduledEnd => wo.ScheduledEnd?.ToString("O"),
|
||||
WorkOrderFieldNames.TargetWeek => wo.TargetWeek?.ToString("O"),
|
||||
WorkOrderFieldNames.ScheduleWeekOnly => wo.ScheduleWeekOnly?.ToString(),
|
||||
WorkOrderFieldNames.SiteCode => wo.SiteCode,
|
||||
WorkOrderFieldNames.Building => wo.Building,
|
||||
WorkOrderFieldNames.Priority => wo.Priority,
|
||||
WorkOrderFieldNames.AssignTo => wo.AssignTo,
|
||||
WorkOrderFieldNames.LocationId => wo.LocationId?.ToString(),
|
||||
WorkOrderFieldNames.LifecycleStatus => wo.LifecycleStatus?.ToString(),
|
||||
WorkOrderFieldNames.WorkOrderType => wo.WorkOrderType?.ToString(),
|
||||
WorkOrderFieldNames.Service => wo.Service,
|
||||
WorkOrderFieldNames.DocStatus => wo.DocStatus?.ToString(),
|
||||
WorkOrderFieldNames.Trade => wo.Trade,
|
||||
WorkOrderFieldNames.Problem => wo.Problem,
|
||||
WorkOrderFieldNames.InternalWONumber => wo.InternalWONumber,
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static bool TryApplyField(WorkOrder wo, string fieldName, object incomingValue)
|
||||
{
|
||||
switch (fieldName)
|
||||
{
|
||||
case WorkOrderFieldNames.Description:
|
||||
wo.Description = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.WorkerOrderTitle:
|
||||
wo.WorkerOrderTitle = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.Status:
|
||||
wo.Status = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.DueDate when TryParseDateTime(incomingValue, out var dueDate):
|
||||
wo.DueDate = dueDate;
|
||||
return true;
|
||||
case WorkOrderFieldNames.ScheduledDate when TryParseDateTime(incomingValue, out var scheduledDate):
|
||||
wo.ScheduledDate = scheduledDate;
|
||||
return true;
|
||||
case WorkOrderFieldNames.ScheduledEnd when TryParseDateTime(incomingValue, out var scheduledEnd):
|
||||
wo.ScheduledEnd = scheduledEnd;
|
||||
return true;
|
||||
case WorkOrderFieldNames.TargetWeek when TryParseDateOnly(incomingValue, out var targetWeek):
|
||||
wo.TargetWeek = targetWeek;
|
||||
return true;
|
||||
case WorkOrderFieldNames.ScheduleWeekOnly when TryParseBool(incomingValue, out var scheduleWeekOnly):
|
||||
wo.ScheduleWeekOnly = scheduleWeekOnly;
|
||||
return true;
|
||||
case WorkOrderFieldNames.SiteCode:
|
||||
wo.SiteCode = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.Building:
|
||||
wo.Building = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.Priority:
|
||||
wo.Priority = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.AssignTo:
|
||||
wo.AssignTo = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.LocationId when TryParseInt(incomingValue, out var locationId):
|
||||
wo.LocationId = locationId;
|
||||
return true;
|
||||
case WorkOrderFieldNames.LifecycleStatus when TryParseEnum(incomingValue, out LifecycleStatus lifecycleStatus):
|
||||
wo.LifecycleStatus = lifecycleStatus;
|
||||
return true;
|
||||
case WorkOrderFieldNames.WorkOrderType when TryParseEnum(incomingValue, out WorkOrderType workOrderType):
|
||||
wo.WorkOrderType = workOrderType;
|
||||
return true;
|
||||
case WorkOrderFieldNames.Service:
|
||||
wo.Service = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.DocStatus when TryParseEnum(incomingValue, out DocStatus docStatus):
|
||||
wo.DocStatus = docStatus;
|
||||
return true;
|
||||
case WorkOrderFieldNames.Trade:
|
||||
wo.Trade = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.Problem:
|
||||
wo.Problem = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.InternalWONumber:
|
||||
wo.InternalWONumber = incomingValue.ToString();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseDateTime(object value, out DateTime result)
|
||||
{
|
||||
if (value is DateTime dt)
|
||||
{
|
||||
result = dt;
|
||||
return true;
|
||||
}
|
||||
|
||||
return DateTime.TryParse(value.ToString(), out result);
|
||||
}
|
||||
|
||||
private static bool TryParseDateOnly(object value, out DateOnly result)
|
||||
{
|
||||
if (value is DateOnly dateOnly)
|
||||
{
|
||||
result = dateOnly;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is DateTime dt)
|
||||
{
|
||||
result = DateOnly.FromDateTime(dt);
|
||||
return true;
|
||||
}
|
||||
|
||||
return DateOnly.TryParse(value.ToString(), out result);
|
||||
}
|
||||
|
||||
private static bool TryParseBool(object value, out bool result)
|
||||
{
|
||||
if (value is bool b)
|
||||
{
|
||||
result = b;
|
||||
return true;
|
||||
}
|
||||
|
||||
return bool.TryParse(value.ToString(), out result);
|
||||
}
|
||||
|
||||
private static bool TryParseInt(object value, out int result)
|
||||
{
|
||||
if (value is int i)
|
||||
{
|
||||
result = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
return int.TryParse(value.ToString(), out result);
|
||||
}
|
||||
|
||||
private static bool TryParseEnum<TEnum>(object value, out TEnum result) where TEnum : struct, Enum
|
||||
{
|
||||
if (value is TEnum enumValue)
|
||||
{
|
||||
result = enumValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
return Enum.TryParse(value.ToString(), true, out result);
|
||||
}
|
||||
}
|
||||
}
|
||||
92
SeaHaven.Services/Implementation/WorkOrderAuditService.cs
Normal file
92
SeaHaven.Services/Implementation/WorkOrderAuditService.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.Services.Constants;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
public class WorkOrderAuditService : IWorkOrderAuditService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IWorkOrderFieldLockService _fieldLocks;
|
||||
|
||||
public WorkOrderAuditService(ApplicationDbContext context, IWorkOrderFieldLockService fieldLocks)
|
||||
{
|
||||
_context = context;
|
||||
_fieldLocks = fieldLocks;
|
||||
}
|
||||
|
||||
public Task LogStatusChangedAsync(int workOrderId, string? oldStatus, string? newStatus, string? actorId)
|
||||
=> LogAsync(workOrderId, AuditActionType.StatusChanged, AuditEventType.Manual,
|
||||
AuditActorType.Dispatcher, WorkOrderFieldNames.LifecycleStatus, oldStatus, newStatus, actorId);
|
||||
|
||||
public Task LogAssignmentChangedAsync(int workOrderId, string? oldValue, string? newValue, string? actorId)
|
||||
=> LogAsync(workOrderId, AuditActionType.AssignmentChanged, AuditEventType.Manual,
|
||||
AuditActorType.Dispatcher, WorkOrderFieldNames.AssignTo, oldValue, newValue, actorId);
|
||||
|
||||
public Task LogSyncRejectedAsync(int workOrderId, string fieldName, string? oldValue, string? newValue)
|
||||
=> LogAsync(workOrderId, AuditActionType.SyncRejected, AuditEventType.Sync,
|
||||
AuditActorType.Sync, fieldName, oldValue, newValue);
|
||||
|
||||
public async Task LogAsync(
|
||||
int workOrderId,
|
||||
AuditActionType action,
|
||||
AuditEventType eventType,
|
||||
AuditActorType actorType,
|
||||
string fieldName,
|
||||
string? oldValue,
|
||||
string? newValue,
|
||||
string? actorId = null,
|
||||
int? dispatchId = null,
|
||||
string? correlationId = null)
|
||||
{
|
||||
_context.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||||
{
|
||||
WorkOrderId = workOrderId,
|
||||
UserId = actorId,
|
||||
FieldName = fieldName,
|
||||
OldValue = oldValue ?? "",
|
||||
NewValue = newValue ?? "",
|
||||
Action = action.ToString(),
|
||||
EventType = eventType.ToString(),
|
||||
ActorType = actorType.ToString(),
|
||||
DispatchId = dispatchId,
|
||||
CorrelationId = correlationId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
if (eventType == AuditEventType.Manual &&
|
||||
(action == AuditActionType.FieldChanged ||
|
||||
action == AuditActionType.StatusChanged ||
|
||||
action == AuditActionType.AssignmentChanged))
|
||||
{
|
||||
await _fieldLocks.LockFieldAsync(workOrderId, fieldName, actorId);
|
||||
}
|
||||
|
||||
await SaveChangesIdempotentAsync();
|
||||
}
|
||||
|
||||
private async Task SaveChangesIdempotentAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex))
|
||||
{
|
||||
foreach (var entry in ex.Entries.Where(e => e.Entity is WorkOrderFieldLock))
|
||||
entry.State = EntityState.Detached;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsUniqueConstraintViolation(DbUpdateException ex)
|
||||
{
|
||||
var message = ex.InnerException?.Message ?? ex.Message;
|
||||
return message.Contains("duplicate", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("UNIQUE", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
public class WorkOrderFieldLockService : IWorkOrderFieldLockService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public WorkOrderFieldLockService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<bool> IsLockedAsync(int workOrderId, string fieldName)
|
||||
{
|
||||
if (IsTracked(workOrderId, fieldName))
|
||||
return true;
|
||||
|
||||
return await _context.WorkOrderFieldLocks
|
||||
.AnyAsync(l => l.WorkOrderId == workOrderId && l.FieldName == fieldName);
|
||||
}
|
||||
|
||||
public async Task LockFieldAsync(int workOrderId, string fieldName, string? userId)
|
||||
{
|
||||
if (IsTracked(workOrderId, fieldName))
|
||||
return;
|
||||
|
||||
if (await IsLockedAsync(workOrderId, fieldName))
|
||||
return;
|
||||
|
||||
_context.WorkOrderFieldLocks.Add(new WorkOrderFieldLock
|
||||
{
|
||||
WorkOrderId = workOrderId,
|
||||
FieldName = fieldName,
|
||||
LockedAt = DateTime.UtcNow,
|
||||
LockedByUserId = userId
|
||||
});
|
||||
}
|
||||
|
||||
private bool IsTracked(int workOrderId, string fieldName)
|
||||
=> _context.WorkOrderFieldLocks.Local.Any(l =>
|
||||
l.WorkOrderId == workOrderId &&
|
||||
string.Equals(l.FieldName, fieldName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
18
SeaHaven.Services/Interfaces/ISyncFieldMergePolicy.cs
Normal file
18
SeaHaven.Services/Interfaces/ISyncFieldMergePolicy.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
namespace SeaHaven.Services.Interfaces
|
||||
{
|
||||
public interface ISyncFieldMergePolicy
|
||||
{
|
||||
IReadOnlySet<string> ShocOwnedFields { get; }
|
||||
|
||||
Task<bool> CanSyncOverwriteAsync(int workOrderId, string fieldName);
|
||||
|
||||
Task<bool> TryApplyAsync(WorkOrderSyncContext context, string fieldName, object? incomingValue);
|
||||
}
|
||||
|
||||
public class WorkOrderSyncContext
|
||||
{
|
||||
public required Data.SeaHavenIndustries.WorkOrder WorkOrder { get; init; }
|
||||
public required SeaHaven.Services.Interfaces.IWorkOrderFieldLockService FieldLocks { get; init; }
|
||||
public required SeaHaven.Services.Interfaces.IWorkOrderAuditService Audit { get; init; }
|
||||
}
|
||||
}
|
||||
23
SeaHaven.Services/Interfaces/IWorkOrderAuditService.cs
Normal file
23
SeaHaven.Services/Interfaces/IWorkOrderAuditService.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.Interfaces
|
||||
{
|
||||
public interface IWorkOrderAuditService
|
||||
{
|
||||
Task LogAsync(
|
||||
int workOrderId,
|
||||
AuditActionType action,
|
||||
AuditEventType eventType,
|
||||
AuditActorType actorType,
|
||||
string fieldName,
|
||||
string? oldValue,
|
||||
string? newValue,
|
||||
string? actorId = null,
|
||||
int? dispatchId = null,
|
||||
string? correlationId = null);
|
||||
|
||||
Task LogStatusChangedAsync(int workOrderId, string? oldStatus, string? newStatus, string? actorId);
|
||||
Task LogAssignmentChangedAsync(int workOrderId, string? oldValue, string? newValue, string? actorId);
|
||||
Task LogSyncRejectedAsync(int workOrderId, string fieldName, string? oldValue, string? newValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
namespace SeaHaven.Services.Interfaces
|
||||
{
|
||||
public interface IWorkOrderFieldLockService
|
||||
{
|
||||
Task LockFieldAsync(int workOrderId, string fieldName, string? userId);
|
||||
Task<bool> IsLockedAsync(int workOrderId, string fieldName);
|
||||
}
|
||||
}
|
||||
50
SeaHavenIndustries.Tests/ConcurrencyExceptionFilterTests.cs
Normal file
50
SeaHavenIndustries.Tests/ConcurrencyExceptionFilterTests.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using Api.SeaHavenIndustries.Filters;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace SeaHavenIndustries.Tests;
|
||||
|
||||
public class ConcurrencyExceptionFilterTests
|
||||
{
|
||||
[Fact]
|
||||
public void OnException_DbUpdateConcurrencyException_Returns409Conflict()
|
||||
{
|
||||
var filter = new ConcurrencyExceptionFilter();
|
||||
var context = CreateExceptionContext(new DbUpdateConcurrencyException());
|
||||
|
||||
filter.OnException(context);
|
||||
|
||||
Assert.True(context.ExceptionHandled);
|
||||
var result = Assert.IsType<ConflictObjectResult>(context.Result);
|
||||
Assert.Equal(StatusCodes.Status409Conflict, result.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnException_OtherException_DoesNotHandle()
|
||||
{
|
||||
var filter = new ConcurrencyExceptionFilter();
|
||||
var context = CreateExceptionContext(new InvalidOperationException("other"));
|
||||
|
||||
filter.OnException(context);
|
||||
|
||||
Assert.False(context.ExceptionHandled);
|
||||
Assert.Null(context.Result);
|
||||
}
|
||||
|
||||
private static ExceptionContext CreateExceptionContext(Exception exception)
|
||||
{
|
||||
var actionContext = new ActionContext(
|
||||
new DefaultHttpContext(),
|
||||
new RouteData(),
|
||||
new ActionDescriptor());
|
||||
|
||||
return new ExceptionContext(actionContext, [])
|
||||
{
|
||||
Exception = exception
|
||||
};
|
||||
}
|
||||
}
|
||||
40
SeaHavenIndustries.Tests/LifecycleStatusMapperTests.cs
Normal file
40
SeaHavenIndustries.Tests/LifecycleStatusMapperTests.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.Constants;
|
||||
using SeaHaven.Services.Helpers;
|
||||
|
||||
namespace SeaHavenIndustries.Tests;
|
||||
|
||||
public class LifecycleStatusMapperTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Open", LifecycleStatus.Incomplete)]
|
||||
[InlineData("Incomplete", LifecycleStatus.Incomplete)]
|
||||
[InlineData("InProgress", LifecycleStatus.InProgress)]
|
||||
[InlineData("In Progress", LifecycleStatus.InProgress)]
|
||||
[InlineData("Completed", LifecycleStatus.Completed)]
|
||||
[InlineData("Cancelled", LifecycleStatus.Canceled)]
|
||||
[InlineData("On Hold", LifecycleStatus.Pending)]
|
||||
[InlineData("Scheduled", LifecycleStatus.Scheduled)]
|
||||
[InlineData("En Route", LifecycleStatus.EnRoute)]
|
||||
[InlineData("Pending Quote", LifecycleStatus.PendingQuote)]
|
||||
public void FromLegacyStatus_MapsKnownValues(string input, LifecycleStatus expected)
|
||||
{
|
||||
var result = LifecycleStatusMapper.FromLegacyStatus(input);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromLegacyStatus_Unknown_ReturnsNull()
|
||||
{
|
||||
var result = LifecycleStatusMapper.FromLegacyStatus("MysteryStatus");
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(LifecycleStatus.EnRoute, "En Route")]
|
||||
[InlineData(LifecycleStatus.PendingQuote, "Pending Quote")]
|
||||
public void ToFeLabel_ReturnsBoardLabel(LifecycleStatus status, string expected)
|
||||
{
|
||||
Assert.Equal(expected, LifecycleStatusMapper.ToFeLabel(status));
|
||||
}
|
||||
}
|
||||
28
SeaHavenIndustries.Tests/SeaHavenIndustries.Tests.csproj
Normal file
28
SeaHavenIndustries.Tests/SeaHavenIndustries.Tests.csproj
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Api.SeaHavenIndustries\Api.SeaHavenIndustries.csproj" />
|
||||
<ProjectReference Include="..\SeaHaven.Services\SeaHaven.Services.csproj" />
|
||||
<ProjectReference Include="..\Data.SeaHavenIndustries\Data.SeaHavenIndustries.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
151
SeaHavenIndustries.Tests/SyncFieldMergePolicyTests.cs
Normal file
151
SeaHavenIndustries.Tests/SyncFieldMergePolicyTests.cs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.Services.Constants;
|
||||
using SeaHaven.Services.Implementation;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHavenIndustries.Tests;
|
||||
|
||||
public class SyncFieldMergePolicyTests
|
||||
{
|
||||
private static ApplicationDbContext CreateContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new ApplicationDbContext(options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryApply_WhenFieldLocked_RejectsAndAudits()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var wo = new WorkOrder { Id = 10, Description = "SHOC edited", Status = "Open" };
|
||||
context.workOrders.Add(wo);
|
||||
context.WorkOrderFieldLocks.Add(new WorkOrderFieldLock
|
||||
{
|
||||
WorkOrderId = 10,
|
||||
FieldName = WorkOrderFieldNames.Description,
|
||||
LockedAt = DateTime.UtcNow
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var locks = new WorkOrderFieldLockService(context);
|
||||
var audit = new WorkOrderAuditService(context, locks);
|
||||
var policy = new SyncFieldMergePolicy(locks);
|
||||
|
||||
var syncContext = new WorkOrderSyncContext
|
||||
{
|
||||
WorkOrder = wo,
|
||||
FieldLocks = locks,
|
||||
Audit = audit
|
||||
};
|
||||
|
||||
var applied = await policy.TryApplyAsync(syncContext, WorkOrderFieldNames.Description, "Sync value");
|
||||
|
||||
Assert.False(applied);
|
||||
Assert.Equal("SHOC edited", wo.Description);
|
||||
|
||||
var logs = await context.WorkOrderAuditLogs.ToListAsync();
|
||||
Assert.Single(logs);
|
||||
Assert.Equal("SyncRejected", logs[0].Action);
|
||||
Assert.Equal("Sync", logs[0].EventType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryApply_WhenFieldNotLocked_AppliesValue()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var wo = new WorkOrder { Id = 11, Description = "Original" };
|
||||
context.workOrders.Add(wo);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var locks = new WorkOrderFieldLockService(context);
|
||||
var audit = new WorkOrderAuditService(context, locks);
|
||||
var policy = new SyncFieldMergePolicy(locks);
|
||||
|
||||
var syncContext = new WorkOrderSyncContext
|
||||
{
|
||||
WorkOrder = wo,
|
||||
FieldLocks = locks,
|
||||
Audit = audit
|
||||
};
|
||||
|
||||
var applied = await policy.TryApplyAsync(syncContext, WorkOrderFieldNames.Description, "From sync");
|
||||
|
||||
Assert.True(applied);
|
||||
Assert.Equal("From sync", wo.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryApply_UnsupportedField_ReturnsFalse()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var wo = new WorkOrder { Id = 12 };
|
||||
context.workOrders.Add(wo);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var locks = new WorkOrderFieldLockService(context);
|
||||
var audit = new WorkOrderAuditService(context, locks);
|
||||
var policy = new SyncFieldMergePolicy(locks);
|
||||
|
||||
var syncContext = new WorkOrderSyncContext
|
||||
{
|
||||
WorkOrder = wo,
|
||||
FieldLocks = locks,
|
||||
Audit = audit
|
||||
};
|
||||
|
||||
var applied = await policy.TryApplyAsync(syncContext, "NotARealField", "value");
|
||||
|
||||
Assert.False(applied);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(WorkOrderFieldNames.LifecycleStatus, LifecycleStatus.Scheduled)]
|
||||
[InlineData(WorkOrderFieldNames.Service, "PM")]
|
||||
[InlineData(WorkOrderFieldNames.DueDate, "2026-07-01T10:00:00Z")]
|
||||
public async Task TryApply_ShocOwnedFields_AppliesValue(string fieldName, object incomingValue)
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var wo = new WorkOrder { Id = 13 };
|
||||
context.workOrders.Add(wo);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var locks = new WorkOrderFieldLockService(context);
|
||||
var audit = new WorkOrderAuditService(context, locks);
|
||||
var policy = new SyncFieldMergePolicy(locks);
|
||||
|
||||
var syncContext = new WorkOrderSyncContext
|
||||
{
|
||||
WorkOrder = wo,
|
||||
FieldLocks = locks,
|
||||
Audit = audit
|
||||
};
|
||||
|
||||
var applied = await policy.TryApplyAsync(syncContext, fieldName, incomingValue);
|
||||
|
||||
Assert.True(applied);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanSyncOverwriteAsync_WhenLocked_ReturnsFalse()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
context.WorkOrderFieldLocks.Add(new WorkOrderFieldLock
|
||||
{
|
||||
WorkOrderId = 20,
|
||||
FieldName = WorkOrderFieldNames.AssignTo,
|
||||
LockedAt = DateTime.UtcNow
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var locks = new WorkOrderFieldLockService(context);
|
||||
var policy = new SyncFieldMergePolicy(locks);
|
||||
|
||||
var canOverwrite = await policy.CanSyncOverwriteAsync(20, WorkOrderFieldNames.AssignTo);
|
||||
|
||||
Assert.False(canOverwrite);
|
||||
}
|
||||
}
|
||||
56
SeaHavenIndustries.Tests/WorkOrderAuditServiceTests.cs
Normal file
56
SeaHavenIndustries.Tests/WorkOrderAuditServiceTests.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.Services.Constants;
|
||||
using SeaHaven.Services.Implementation;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHavenIndustries.Tests;
|
||||
|
||||
public class WorkOrderAuditServiceTests
|
||||
{
|
||||
private static ApplicationDbContext CreateContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new ApplicationDbContext(options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LogStatusChanged_CreatesSingleAuditWithManualEventType()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
context.workOrders.Add(new WorkOrder { Id = 1, Status = "Open" });
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var locks = new WorkOrderFieldLockService(context);
|
||||
var audit = new WorkOrderAuditService(context, locks);
|
||||
|
||||
await audit.LogStatusChangedAsync(1, "Open", "In Progress", "user-1");
|
||||
|
||||
var logs = await context.WorkOrderAuditLogs.ToListAsync();
|
||||
Assert.Single(logs);
|
||||
Assert.Equal("StatusChanged", logs[0].Action);
|
||||
Assert.Equal("Manual", logs[0].EventType);
|
||||
Assert.Equal("Dispatcher", logs[0].ActorType);
|
||||
Assert.Equal(WorkOrderFieldNames.LifecycleStatus, logs[0].FieldName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LogStatusChanged_CreatesFieldLock()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
context.workOrders.Add(new WorkOrder { Id = 2, Status = "Open" });
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var locks = new WorkOrderFieldLockService(context);
|
||||
var audit = new WorkOrderAuditService(context, locks);
|
||||
|
||||
await audit.LogStatusChangedAsync(2, "Open", "Scheduled", "user-1");
|
||||
|
||||
var fieldLocks = await context.WorkOrderFieldLocks.ToListAsync();
|
||||
Assert.Single(fieldLocks);
|
||||
Assert.Equal(WorkOrderFieldNames.LifecycleStatus, fieldLocks[0].FieldName);
|
||||
}
|
||||
}
|
||||
75
SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs
Normal file
75
SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.Helpers;
|
||||
|
||||
namespace SeaHavenIndustries.Tests;
|
||||
|
||||
public class WorkOrderDerivedFieldsTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsPastDue_WhenScheduledBeforeTodayAndNotTerminal_ReturnsTrue()
|
||||
{
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
ScheduledDate = DateTime.UtcNow.AddDays(-2),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled
|
||||
};
|
||||
|
||||
Assert.True(WorkOrderDerivedFields.IsPastDue(wo, DateOnly.FromDateTime(DateTime.UtcNow)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPastDue_WhenCompleted_ReturnsFalse()
|
||||
{
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
ScheduledDate = DateTime.UtcNow.AddDays(-2),
|
||||
LifecycleStatus = LifecycleStatus.Completed
|
||||
};
|
||||
|
||||
Assert.False(WorkOrderDerivedFields.IsPastDue(wo, DateOnly.FromDateTime(DateTime.UtcNow)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyAutoSchedule_WhenDateAndAssignee_PromotesToScheduled()
|
||||
{
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
LifecycleStatus = LifecycleStatus.Incomplete,
|
||||
ScheduledDate = new DateTime(2026, 6, 25),
|
||||
AssignTo = "dispatcher-1"
|
||||
};
|
||||
|
||||
Assert.True(WorkOrderDerivedFields.ApplyAutoScheduleIfEligible(wo));
|
||||
Assert.Equal(LifecycleStatus.Scheduled, wo.LifecycleStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyAutoSchedule_WeekOnly_DoesNotPromote()
|
||||
{
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
LifecycleStatus = LifecycleStatus.Incomplete,
|
||||
ScheduledDate = new DateTime(2026, 6, 25),
|
||||
AssignTo = "dispatcher-1",
|
||||
ScheduleWeekOnly = true,
|
||||
TargetWeek = new DateOnly(2026, 6, 22)
|
||||
};
|
||||
|
||||
Assert.False(WorkOrderDerivedFields.ApplyAutoScheduleIfEligible(wo));
|
||||
Assert.Equal(LifecycleStatus.Incomplete, wo.LifecycleStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetInitialLifecycleStatus_NewWo_StartsIncompleteThenAutoSchedules()
|
||||
{
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
ScheduledDate = new DateTime(2026, 6, 25),
|
||||
AssignTo = "dispatcher-1"
|
||||
};
|
||||
|
||||
WorkOrderDerivedFields.SetInitialLifecycleStatus(wo);
|
||||
Assert.Equal(LifecycleStatus.Scheduled, wo.LifecycleStatus);
|
||||
}
|
||||
}
|
||||
53
SeaHavenIndustries.Tests/WorkOrderFieldLockServiceTests.cs
Normal file
53
SeaHavenIndustries.Tests/WorkOrderFieldLockServiceTests.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.Services.Constants;
|
||||
using SeaHaven.Services.Implementation;
|
||||
|
||||
namespace SeaHavenIndustries.Tests;
|
||||
|
||||
public class WorkOrderFieldLockServiceTests
|
||||
{
|
||||
private static ApplicationDbContext CreateContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new ApplicationDbContext(options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LockFieldAsync_DuplicateCalls_EnqueuesSingleLock()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var locks = new WorkOrderFieldLockService(context);
|
||||
|
||||
await locks.LockFieldAsync(1, WorkOrderFieldNames.Description, "user-1");
|
||||
await locks.LockFieldAsync(1, WorkOrderFieldNames.Description, "user-2");
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var fieldLocks = await context.WorkOrderFieldLocks.ToListAsync();
|
||||
Assert.Single(fieldLocks);
|
||||
Assert.Equal(WorkOrderFieldNames.Description, fieldLocks[0].FieldName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LockFieldAsync_WhenAlreadyPersisted_IsIdempotent()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
context.WorkOrderFieldLocks.Add(new WorkOrderFieldLock
|
||||
{
|
||||
WorkOrderId = 2,
|
||||
FieldName = WorkOrderFieldNames.AssignTo,
|
||||
LockedAt = DateTime.UtcNow,
|
||||
LockedByUserId = "user-1"
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var locks = new WorkOrderFieldLockService(context);
|
||||
await locks.LockFieldAsync(2, WorkOrderFieldNames.AssignTo, "user-2");
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var fieldLocks = await context.WorkOrderFieldLocks.ToListAsync();
|
||||
Assert.Single(fieldLocks);
|
||||
}
|
||||
}
|
||||
43
SeaHavenIndustries.Tests/WorkOrderNumberNormalizerTests.cs
Normal file
43
SeaHavenIndustries.Tests/WorkOrderNumberNormalizerTests.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using SeaHaven.Services.Helpers;
|
||||
|
||||
namespace SeaHavenIndustries.Tests;
|
||||
|
||||
public class WorkOrderNumberNormalizerTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryNormalize_PadsToElevenDigits()
|
||||
{
|
||||
var ok = WorkOrderNumberNormalizer.TryNormalize("12345", out var normalized, out var error);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Null(error);
|
||||
Assert.Equal("00000012345", normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryNormalize_AcceptsElevenDigits()
|
||||
{
|
||||
var ok = WorkOrderNumberNormalizer.TryNormalize("10000000001", out var normalized, out _);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal("10000000001", normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryNormalize_RejectsEmpty()
|
||||
{
|
||||
var ok = WorkOrderNumberNormalizer.TryNormalize("", out _, out var error);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryNormalize_RejectsTooLong()
|
||||
{
|
||||
var ok = WorkOrderNumberNormalizer.TryNormalize("123456789012", out _, out var error);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.Contains("11", error);
|
||||
}
|
||||
}
|
||||
32
SeaHavenIndustries.Tests/WorkOrderTypeMapperTests.cs
Normal file
32
SeaHavenIndustries.Tests/WorkOrderTypeMapperTests.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.Helpers;
|
||||
|
||||
namespace SeaHavenIndustries.Tests;
|
||||
|
||||
public class WorkOrderTypeMapperTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("PM", WorkOrderType.PM)]
|
||||
[InlineData("Reactive", WorkOrderType.Reactive)]
|
||||
[InlineData("Emergency", WorkOrderType.Emergency)]
|
||||
[InlineData("Add-On", WorkOrderType.AddOn)]
|
||||
public void TryParseFeLabel_BoardTypes(string label, WorkOrderType expected)
|
||||
{
|
||||
Assert.True(WorkOrderTypeMapper.TryParseFeLabel(label, out var type));
|
||||
Assert.Equal(expected, type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseFeLabel_Overdue_ReturnsFalse()
|
||||
{
|
||||
Assert.False(WorkOrderTypeMapper.TryParseFeLabel("Overdue", out _));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(WorkOrderType.AddOn, "Add-On")]
|
||||
[InlineData(WorkOrderType.Reactive, "Reactive")]
|
||||
public void ToFeLabel_ReturnsBoardLabel(WorkOrderType type, string expected)
|
||||
{
|
||||
Assert.Equal(expected, WorkOrderTypeMapper.ToFeLabel(type));
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.SeaHavenIndustries.Test
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeaHaven.Services.Tests", "SeaHaven.Services.Tests\SeaHaven.Services.Tests.csproj", "{97455198-C479-47F2-8BC8-601B335DFFAB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeaHavenIndustries.Tests", "SeaHavenIndustries.Tests\SeaHavenIndustries.Tests.csproj", "{0093B3F5-0180-4C02-A088-6A564E06CEBF}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -51,6 +53,10 @@ Global
|
|||
{97455198-C479-47F2-8BC8-601B335DFFAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{97455198-C479-47F2-8BC8-601B335DFFAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{97455198-C479-47F2-8BC8-601B335DFFAB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0093B3F5-0180-4C02-A088-6A564E06CEBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0093B3F5-0180-4C02-A088-6A564E06CEBF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0093B3F5-0180-4C02-A088-6A564E06CEBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0093B3F5-0180-4C02-A088-6A564E06CEBF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue