diff --git a/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs b/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs index 1f86f21..5b355c4 100644 --- a/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs +++ b/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.VisualBasic; @@ -17,6 +18,7 @@ using System.Security.Claims; using static System.Runtime.InteropServices.JavaScript.JSType; using SeaHaven.Services.Interfaces; using SeaHaven.Services.DTOs; +using SeaHaven.Services.Helpers; using SeaHaven.DataServices.Interfaces; namespace Api.SeaHavenIndustries.Controllers @@ -42,6 +44,7 @@ namespace Api.SeaHavenIndustries.Controllers private readonly Helper.SendMessage _sendMessage; private readonly Helper.VendorPortalTokenService _vendorTokens; private readonly IConfiguration _config; + private readonly IWorkOrderBoardService _workOrderBoardService; public WorkOrderController( UserManager userManager, @@ -58,7 +61,8 @@ namespace Api.SeaHavenIndustries.Controllers IHttpContextAccessor httpContext, Helper.SendMessage sendMessage, Helper.VendorPortalTokenService vendorTokens, - IConfiguration config) + IConfiguration config, + IWorkOrderBoardService workOrderBoardService) { _userManager = userManager; _workOrderService = workOrderService; @@ -75,7 +79,62 @@ namespace Api.SeaHavenIndustries.Controllers _sendMessage = sendMessage; _vendorTokens = vendorTokens; _config = config; + _workOrderBoardService = workOrderBoardService; } + + /// + /// Weekly board payload. + /// When is true, the authenticated user filter wins and + /// is ignored (not AND-combined). + /// Weekend WOs in a Mon–Sun window may have dayGroup = null; FE should handle that. + /// + [HttpGet("board")] + public async Task GetBoard( + [FromQuery, BindRequired] DateOnly weekStart, + [FromQuery] DateOnly? weekEnd = null, + [FromQuery] List? dispatchers = null, + [FromQuery] bool myWorkOrders = false, + [FromQuery] List? types = null, + [FromQuery] string? search = null) + { + var resolvedWeekEnd = weekEnd ?? weekStart.AddDays(4); + var weekValidationError = WorkOrderOperationalWeek.ValidateWeekWindow(weekStart, resolvedWeekEnd); + if (weekValidationError != null) + return BadRequest(weekValidationError); + + try + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var query = new WorkOrderBoardQueryDto + { + WeekStart = weekStart, + WeekEnd = weekEnd, + Dispatchers = dispatchers, + MyWorkOrders = myWorkOrders, + Types = types, + Search = search + }; + + var result = await _workOrderBoardService.GetBoardAsync(query, userId); + return Ok(result); + } + catch (ArgumentException ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// Dispatcher dropdown options. Currently returns all non-deleted users + /// (role-filtered assignee list deferred until roles settle). + /// + [HttpGet("lookups/dispatchers")] + public async Task GetDispatcherLookups() + { + var result = await _workOrderBoardService.GetDispatcherLookupsAsync(); + return Ok(result); + } + [HttpPost] [Route("AddWorkorder")] public async Task Addworkorder([FromForm] Workorder_DTO model) diff --git a/Api.SeaHavenIndustries/Filters/ArgumentExceptionFilter.cs b/Api.SeaHavenIndustries/Filters/ArgumentExceptionFilter.cs new file mode 100644 index 0000000..0bfeabb --- /dev/null +++ b/Api.SeaHavenIndustries/Filters/ArgumentExceptionFilter.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Api.SeaHavenIndustries.Filters +{ + public class ArgumentExceptionFilter : IExceptionFilter + { + public void OnException(ExceptionContext context) + { + if (context.Exception is not ArgumentException ex) return; + + context.Result = new BadRequestObjectResult(new + { + status = "Error", + message = ex.Message, + code = 400 + }); + context.ExceptionHandled = true; + } + } +} diff --git a/Api.SeaHavenIndustries/Program.cs b/Api.SeaHavenIndustries/Program.cs index 17022c8..d7efbeb 100644 --- a/Api.SeaHavenIndustries/Program.cs +++ b/Api.SeaHavenIndustries/Program.cs @@ -25,6 +25,7 @@ builder.Services.AddIdentity(options => builder.Services.AddControllers(options => { options.Filters.Add(); + options.Filters.Add(); }).AddJsonOptions(options => { options.JsonSerializerOptions.MaxDepth = 264; // or any other desired value diff --git a/Data.SeaHavenIndustries/Enums/LifecycleStatusSets.cs b/Data.SeaHavenIndustries/Enums/LifecycleStatusSets.cs new file mode 100644 index 0000000..d4a93bc --- /dev/null +++ b/Data.SeaHavenIndustries/Enums/LifecycleStatusSets.cs @@ -0,0 +1,14 @@ +namespace Data.SeaHavenIndustries.Enums +{ + /// + /// Shared terminal lifecycle statuses for EF-translatable filters and board rules. + /// + public static class LifecycleStatusSets + { + public static readonly LifecycleStatus?[] Terminal = + { + LifecycleStatus.Completed, + LifecycleStatus.Canceled + }; + } +} diff --git a/Data.SeaHavenIndustries/Migrations/20260701120000_Phase1_BoardIndexes.Designer.cs b/Data.SeaHavenIndustries/Migrations/20260701120000_Phase1_BoardIndexes.Designer.cs new file mode 100644 index 0000000..ba49c97 --- /dev/null +++ b/Data.SeaHavenIndustries/Migrations/20260701120000_Phase1_BoardIndexes.Designer.cs @@ -0,0 +1,2996 @@ +// +using System; +using Data.SeaHavenIndustries; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.SeaHavenIndustries.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260701120000_Phase1_BoardIndexes")] + partial class Phase1_BoardIndexes + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Data.SeaHavenIndustries.Accounts", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("About") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("CurrencyCode") + .HasColumnType("nvarchar(max)"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("FacebookUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Importance") + .HasColumnType("int"); + + b.Property("IndustryId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LinkedInUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerEmployeeId") + .HasColumnType("int"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("PrivacySetting") + .HasColumnType("nvarchar(max)"); + + b.Property("ServiceInstructions") + .HasColumnType("nvarchar(max)"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("TerritoryId") + .HasColumnType("int"); + + b.Property("TimeZone") + .HasColumnType("nvarchar(max)"); + + b.Property("TwitterUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Website") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Addresses", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("Address1") + .HasColumnType("nvarchar(max)"); + + b.Property("Address2") + .HasColumnType("nvarchar(max)"); + + b.Property("AddressType") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactId") + .HasColumnType("int"); + + b.Property("Country") + .HasColumnType("nvarchar(max)"); + + b.Property("County") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("Zip") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ContactId"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Color") + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Contact") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeletedDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("Initials") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("Locations") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UniqueName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserImage") + .HasColumnType("nvarchar(max)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Assets", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("AssetCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("LocationId"); + + b.ToTable("Assets"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Comments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CommentType") + .HasColumnType("nvarchar(max)"); + + b.Property("Commenter") + .HasColumnType("nvarchar(max)"); + + b.Property("Commenttext") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("Documents") + .HasColumnType("nvarchar(max)"); + + b.Property("ExternalCommentId") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("RecordType") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("WorkerOrderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DispatchId"); + + b.HasIndex("UserId"); + + b.HasIndex("WorkerOrderId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.ContactDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("About") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DateOfBirth") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("FacebookUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Gender") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LinkedInUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("PrivacySetting") + .HasColumnType("nvarchar(max)"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("Territory") + .HasColumnType("nvarchar(max)"); + + b.Property("TwitterUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactDetails"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Contacts", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("Address1") + .HasColumnType("nvarchar(max)"); + + b.Property("Address2") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactType") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("FacebookUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("LinkedInUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("MiddleName") + .HasColumnType("nvarchar(max)"); + + b.Property("Owner") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("TwitterUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Zip") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("LocationId"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Dispatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AcceptToken") + .HasColumnType("nvarchar(max)"); + + b.Property("AcknowledgedAt") + .HasColumnType("datetime2"); + + b.Property("CompletedDate") + .HasColumnType("datetime2"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DispatchNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("DispatchedAt") + .HasColumnType("datetime2"); + + b.Property("EmailSent") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("NTEAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("PONumber") + .HasColumnType("nvarchar(max)"); + + b.Property("ReplyToAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("VerifiedAt") + .HasColumnType("datetime2"); + + b.Property("VerifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("VendorId"); + + b.HasIndex("WorkOrderId"); + + b.ToTable("Dispatches"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchChecklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("CompletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("IsCompleted") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ItemText") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DispatchId"); + + b.HasIndex("WorkOrderId"); + + b.ToTable("DispatchChecklistItems"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchSignoff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Signature") + .HasColumnType("nvarchar(max)"); + + b.Property("SignatureMethod") + .HasColumnType("nvarchar(max)"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignoffType") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DispatchId"); + + b.ToTable("DispatchSignoffs"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchUpliftRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("CurrentNTE") + .HasColumnType("decimal(18,2)"); + + b.Property("DecidedAt") + .HasColumnType("datetime2"); + + b.Property("DecidedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DecisionNote") + .HasColumnType("nvarchar(max)"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("RequestedByVendorName") + .HasColumnType("nvarchar(max)"); + + b.Property("RequestedNTE") + .HasColumnType("decimal(18,2)"); + + b.Property("RequiredTier") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("VendorReason") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DispatchId", "Status") + .IsUnique() + .HasFilter("[Status] = 'Pending'"); + + b.ToTable("DispatchUpliftRequests"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DispatchId"); + + b.HasIndex("WorkOrderId"); + + b.ToTable("DispatchWorkOrders"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DropdownOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("ParentValue") + .HasColumnType("nvarchar(max)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DropdownOptions"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("About") + .HasColumnType("nvarchar(max)"); + + b.Property("CanChangeProviderNumber") + .HasColumnType("bit"); + + b.Property("ConfirmUserName") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DateOfBirth") + .HasColumnType("datetime2"); + + b.Property("DepartmentId") + .HasColumnType("int"); + + b.Property("EmployeeNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("FacebookUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("HireDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobTitleId") + .HasColumnType("int"); + + b.Property("LastLogin") + .HasColumnType("datetime2"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LinkedInUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("MiddleName") + .HasColumnType("nvarchar(max)"); + + b.Property("PermissionGroupId") + .HasColumnType("int"); + + b.Property("RegionId") + .HasColumnType("int"); + + b.Property("SMSAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TimeZone") + .HasColumnType("nvarchar(max)"); + + b.Property("TwitterUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("UserName") + .HasColumnType("nvarchar(max)"); + + b.Property("Website") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("JobTitleId"); + + b.HasIndex("RegionId"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.EmployeeAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CountryCode") + .HasColumnType("nvarchar(max)"); + + b.Property("CountyId") + .HasColumnType("int"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("StateCode") + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.Property("StreetLineTwo") + .HasColumnType("nvarchar(max)"); + + b.Property("Zip") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.ToTable("EmployeeAddresses"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.EmployeeEmail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailTypeId") + .HasColumnType("int"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("IsPrimary") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EmployeeEmails"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.EmployeePhone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("Extension") + .HasColumnType("nvarchar(max)"); + + b.Property("IsPrimary") + .HasColumnType("bit"); + + b.Property("Number") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneTypeId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EmployeePhones"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.FollowUps", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ScheduleDate") + .HasColumnType("datetime2"); + + b.Property("ScheduleStartTime") + .HasColumnType("time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("LocationId"); + + b.HasIndex("WorkOrderId"); + + b.ToTable("FollowUps"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.ForgetPasswordCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ForgetPasswordCodes"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.JobTitle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("JobTitles"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Locations", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address1") + .HasColumnType("nvarchar(max)"); + + b.Property("Address2") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Latitude") + .HasColumnType("nvarchar(max)"); + + b.Property("Longitude") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("Zip") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.PMSchedules", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AssetId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Frequency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AssetId"); + + b.HasIndex("LocationId"); + + b.ToTable("PMSchedules"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Quotes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BillingAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("PO") + .HasColumnType("nvarchar(max)"); + + b.Property("ProjectName") + .HasColumnType("nvarchar(max)"); + + b.Property("QuotId") + .HasColumnType("nvarchar(max)"); + + b.Property("ShippingAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("SiteId") + .HasColumnType("nvarchar(max)"); + + b.Property("SiteName") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TT") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkorderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("WorkorderId"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.QuotesLineItems", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Descriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Lineitem") + .HasColumnType("decimal(18,2)"); + + b.Property("LineitemNumber") + .HasColumnType("int"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("QuoteId"); + + b.ToTable("QuotesLines"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Regions"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.TaskListTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TaskListTemplates"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.TaskListTemplateItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ItemText") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TaskListTemplateId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("TaskListTemplateId"); + + b.ToTable("TaskListTemplateItems"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Template", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AfterPhotoAttachment") + .HasColumnType("nvarchar(max)"); + + b.Property("AfterPhotoIssue") + .HasColumnType("nvarchar(max)"); + + b.Property("AssignTo") + .HasColumnType("nvarchar(450)"); + + b.Property("Attachments") + .HasColumnType("nvarchar(max)"); + + b.Property("BeforPhotoAttachment") + .HasColumnType("nvarchar(max)"); + + b.Property("BeforPhotoIssue") + .HasColumnType("nvarchar(max)"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ContactId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("PO") + .HasColumnType("nvarchar(max)"); + + b.Property("Priority") + .HasColumnType("nvarchar(max)"); + + b.Property("SignOffAttachment") + .HasColumnType("nvarchar(max)"); + + b.Property("SignOffName") + .HasColumnType("nvarchar(max)"); + + b.Property("SignOffSignature") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TT") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkerOrderNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkerOrderTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AssignTo"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ContactId"); + + b.HasIndex("LocationId"); + + b.ToTable("Templates"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Vendor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyName") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactName") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("TradeSpecialties") + .HasColumnType("nvarchar(max)"); + + b.Property("Zip") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Vendors"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.VendorAccessToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IssuedAt") + .HasColumnType("datetime2"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LastUsedAt") + .HasColumnType("datetime2"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("VendorId"); + + b.ToTable("VendorAccessTokens"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AfterPhotoAttachment") + .HasColumnType("nvarchar(max)"); + + b.Property("AfterPhotoIssue") + .HasColumnType("nvarchar(max)"); + + b.Property("AssignDate") + .HasColumnType("date"); + + b.Property("AssignTo") + .HasColumnType("nvarchar(450)"); + + b.Property("Attachments") + .HasColumnType("nvarchar(max)"); + + b.Property("AvettaTask") + .HasColumnType("nvarchar(max)"); + + b.Property("BeforPhotoAttachment") + .HasColumnType("nvarchar(max)"); + + b.Property("BeforPhotoIssue") + .HasColumnType("nvarchar(max)"); + + b.Property("Building") + .HasColumnType("nvarchar(max)"); + + b.Property("CarriedOver") + .HasColumnType("int"); + + b.Property("CompletedDate") + .HasColumnType("datetime2"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Customer") + .HasColumnType("nvarchar(max)"); + + b.Property("DateReported") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DocStatus") + .HasColumnType("int"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("ExternalWorkOrderId") + .HasColumnType("nvarchar(max)"); + + b.Property("InternalWONumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LegacyStatus") + .HasColumnType("nvarchar(max)"); + + b.Property("LifecycleStatus") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("OperationalFlags") + .HasColumnType("int"); + + b.Property("OriginalDate") + .HasColumnType("date"); + + b.Property("OriginalWeek") + .HasColumnType("date"); + + b.Property("PO") + .HasColumnType("nvarchar(max)"); + + b.Property("PrimaryDispatchId") + .HasColumnType("int"); + + b.Property("Priority") + .HasColumnType("nvarchar(max)"); + + b.Property("Problem") + .HasColumnType("nvarchar(max)"); + + b.Property("RescheduleCount") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion"); + + b.Property("ScheduleWeekOnly") + .HasColumnType("bit"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("ScheduledEnd") + .HasColumnType("datetime2"); + + b.Property("ScheduledStart") + .HasColumnType("datetime2"); + + b.Property("Severity") + .HasColumnType("nvarchar(max)"); + + b.Property("Service") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("SignOffAttachment") + .HasColumnType("nvarchar(max)"); + + b.Property("SignOffName") + .HasColumnType("nvarchar(max)"); + + b.Property("SignOffSignature") + .HasColumnType("nvarchar(max)"); + + b.Property("SiteCode") + .HasColumnType("nvarchar(max)"); + + b.Property("Source") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceEmailS3Key") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("SubTrade") + .HasColumnType("nvarchar(max)"); + + b.Property("TT") + .HasColumnType("nvarchar(max)"); + + b.Property("TargetWeek") + .HasColumnType("date"); + + b.Property("Trade") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorNTE") + .HasColumnType("decimal(18,2)"); + + b.Property("WorkOrderType") + .HasColumnType("int"); + + b.Property("WorkerOrderNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkerOrderTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.Property("istemplate") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AssignTo"); + + b.HasIndex("AssignTo", "ScheduledDate"); + + b.HasIndex("LocationId"); + + b.HasIndex("PrimaryDispatchId"); + + b.HasIndex("ScheduledDate"); + + b.HasIndex("WorkOrderType"); + + b.ToTable("workOrders"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderAttachments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attachments") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("WorkorderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("WorkorderId"); + + b.ToTable("workOrderAttachments"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderAuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("nvarchar(max)"); + + b.Property("ActorType") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("EventType") + .HasColumnType("nvarchar(max)"); + + b.Property("FieldName") + .HasColumnType("nvarchar(max)"); + + b.Property("NewValue") + .HasColumnType("nvarchar(max)"); + + b.Property("OldValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("WorkOrderId"); + + b.ToTable("WorkOrderAuditLogs"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderCategories", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("WorkorderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("WorkorderId"); + + b.ToTable("workOrderCategories"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderContacts", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContactId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkorderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("WorkorderId"); + + b.ToTable("WorkOrderContacts"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderFieldLock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FieldName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("LockedAt") + .HasColumnType("datetime2"); + + b.Property("LockedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkOrderId", "FieldName") + .IsUnique(); + + b.ToTable("WorkOrderFieldLocks"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Addresses", b => + { + b.HasOne("Data.SeaHavenIndustries.Accounts", "Account") + .WithMany("Addresses") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Contacts", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Account"); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Assets", b => + { + b.HasOne("Data.SeaHavenIndustries.Accounts", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Locations", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Account"); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Comments", b => + { + b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch") + .WithMany("Comments") + .HasForeignKey("DispatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", "ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("Comments") + .HasForeignKey("WorkerOrderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ApplicationUser"); + + b.Navigation("Dispatch"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.ContactDetails", b => + { + b.HasOne("Data.SeaHavenIndustries.Contacts", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Contacts", b => + { + b.HasOne("Data.SeaHavenIndustries.Accounts", "Account") + .WithMany("Contacts") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Locations", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Account"); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Dispatch", b => + { + b.HasOne("Data.SeaHavenIndustries.Vendor", "Vendor") + .WithMany("Dispatches") + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("Dispatches") + .HasForeignKey("WorkOrderId"); + + b.Navigation("Vendor"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchChecklistItem", b => + { + b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch") + .WithMany("ChecklistItems") + .HasForeignKey("DispatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany() + .HasForeignKey("WorkOrderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Dispatch"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchSignoff", b => + { + b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch") + .WithMany("Signoffs") + .HasForeignKey("DispatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dispatch"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchUpliftRequest", b => + { + b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch") + .WithMany() + .HasForeignKey("DispatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dispatch"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b => + { + b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch") + .WithMany("DispatchWorkOrders") + .HasForeignKey("DispatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany() + .HasForeignKey("WorkOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dispatch"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Employee", b => + { + b.HasOne("Data.SeaHavenIndustries.Department", "Department") + .WithMany("Employees") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.JobTitle", "JobTitle") + .WithMany("Employees") + .HasForeignKey("JobTitleId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Region", "Region") + .WithMany("Employees") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Department"); + + b.Navigation("JobTitle"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.EmployeeAddress", b => + { + b.HasOne("Data.SeaHavenIndustries.Employee", "Employee") + .WithOne("Address") + .HasForeignKey("Data.SeaHavenIndustries.EmployeeAddress", "EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.EmployeeEmail", b => + { + b.HasOne("Data.SeaHavenIndustries.Employee", "Employee") + .WithMany("Emails") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.EmployeePhone", b => + { + b.HasOne("Data.SeaHavenIndustries.Employee", "Employee") + .WithMany("Phones") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.FollowUps", b => + { + b.HasOne("Data.SeaHavenIndustries.Accounts", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Locations", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany() + .HasForeignKey("WorkOrderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Account"); + + b.Navigation("Employee"); + + b.Navigation("Location"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.PMSchedules", b => + { + b.HasOne("Data.SeaHavenIndustries.Assets", "Asset") + .WithMany("PMSchedules") + .HasForeignKey("AssetId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Locations", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Asset"); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Quotes", b => + { + b.HasOne("Data.SeaHavenIndustries.Contacts", "Contacts") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("Quotes") + .HasForeignKey("WorkorderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Contacts"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.QuotesLineItems", b => + { + b.HasOne("Data.SeaHavenIndustries.Quotes", "Quotes") + .WithMany("LineItems") + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Quotes"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.TaskListTemplateItem", b => + { + b.HasOne("Data.SeaHavenIndustries.TaskListTemplate", "Template") + .WithMany("Items") + .HasForeignKey("TaskListTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Template", b => + { + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", "AssignToUser") + .WithMany("Templates") + .HasForeignKey("AssignTo") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Category", "Category") + .WithMany("Templates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Contacts", "POC") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Locations", "Locations") + .WithMany("Templates") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignToUser"); + + b.Navigation("Category"); + + b.Navigation("Locations"); + + b.Navigation("POC"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.VendorAccessToken", b => + { + b.HasOne("Data.SeaHavenIndustries.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrder", b => + { + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", "AssignToUser") + .WithMany("WorkOrders") + .HasForeignKey("AssignTo") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Locations", "Locations") + .WithMany("workOrders") + .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 => + { + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("workOrderAttachments") + .HasForeignKey("WorkorderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderAuditLog", b => + { + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany() + .HasForeignKey("WorkOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderCategories", b => + { + b.HasOne("Data.SeaHavenIndustries.Category", "Category") + .WithMany("WorkOrderCategories") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("WorkOrderCategories") + .HasForeignKey("WorkorderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Category"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderContacts", b => + { + b.HasOne("Data.SeaHavenIndustries.Contacts", "POC") + .WithMany("WorkOrderContacts") + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("WorkOrderContacts") + .HasForeignKey("WorkorderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("POC"); + + 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", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Accounts", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.ApplicationUser", b => + { + b.Navigation("Templates"); + + b.Navigation("WorkOrders"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Assets", b => + { + b.Navigation("PMSchedules"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Category", b => + { + b.Navigation("Templates"); + + b.Navigation("WorkOrderCategories"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Contacts", b => + { + b.Navigation("WorkOrderContacts"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Department", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Dispatch", b => + { + b.Navigation("ChecklistItems"); + + b.Navigation("Comments"); + + b.Navigation("DispatchWorkOrders"); + + b.Navigation("Signoffs"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Employee", b => + { + b.Navigation("Address"); + + b.Navigation("Emails"); + + b.Navigation("Phones"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.JobTitle", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Locations", b => + { + b.Navigation("Templates"); + + b.Navigation("workOrders"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Quotes", b => + { + b.Navigation("LineItems"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Region", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.TaskListTemplate", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Vendor", b => + { + b.Navigation("Dispatches"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrder", b => + { + b.Navigation("Comments"); + + b.Navigation("Dispatches"); + + b.Navigation("FieldLocks"); + + b.Navigation("Quotes"); + + b.Navigation("WorkOrderCategories"); + + b.Navigation("WorkOrderContacts"); + + b.Navigation("workOrderAttachments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data.SeaHavenIndustries/Migrations/20260701120000_Phase1_BoardIndexes.cs b/Data.SeaHavenIndustries/Migrations/20260701120000_Phase1_BoardIndexes.cs new file mode 100644 index 0000000..744a9fa --- /dev/null +++ b/Data.SeaHavenIndustries/Migrations/20260701120000_Phase1_BoardIndexes.cs @@ -0,0 +1,64 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.SeaHavenIndustries.Migrations +{ + /// + public partial class Phase1_BoardIndexes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + IF NOT EXISTS ( + SELECT 1 FROM sys.indexes + WHERE name = 'IX_workOrders_ScheduledDate' + AND object_id = OBJECT_ID('workOrders')) + BEGIN + CREATE INDEX IX_workOrders_ScheduledDate ON workOrders (ScheduledDate); + END + + IF NOT EXISTS ( + SELECT 1 FROM sys.indexes + WHERE name = 'IX_workOrders_AssignTo_ScheduledDate' + AND object_id = OBJECT_ID('workOrders')) + BEGIN + CREATE INDEX IX_workOrders_AssignTo_ScheduledDate ON workOrders (AssignTo, ScheduledDate); + END + + IF NOT EXISTS ( + SELECT 1 FROM sys.indexes + WHERE name = 'IX_workOrders_WorkOrderType' + AND object_id = OBJECT_ID('workOrders')) + BEGIN + CREATE INDEX IX_workOrders_WorkOrderType ON workOrders (WorkOrderType); + END + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + IF EXISTS ( + SELECT 1 FROM sys.indexes + WHERE name = 'IX_workOrders_WorkOrderType' + AND object_id = OBJECT_ID('workOrders')) + DROP INDEX IX_workOrders_WorkOrderType ON workOrders; + + IF EXISTS ( + SELECT 1 FROM sys.indexes + WHERE name = 'IX_workOrders_AssignTo_ScheduledDate' + AND object_id = OBJECT_ID('workOrders')) + DROP INDEX IX_workOrders_AssignTo_ScheduledDate ON workOrders; + + IF EXISTS ( + SELECT 1 FROM sys.indexes + WHERE name = 'IX_workOrders_ScheduledDate' + AND object_id = OBJECT_ID('workOrders')) + DROP INDEX IX_workOrders_ScheduledDate ON workOrders; + """); + } + } +} diff --git a/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs b/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs index 33dd3a9..11bc0cf 100644 --- a/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs @@ -2403,10 +2403,16 @@ namespace Data.SeaHavenIndustries.Migrations b.HasIndex("AssignTo"); + b.HasIndex("AssignTo", "ScheduledDate"); + b.HasIndex("LocationId"); b.HasIndex("PrimaryDispatchId"); + b.HasIndex("ScheduledDate"); + + b.HasIndex("WorkOrderType"); + b.ToTable("workOrders"); }); diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs new file mode 100644 index 0000000..75b98df --- /dev/null +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs @@ -0,0 +1,102 @@ +using Data.SeaHavenIndustries; +using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Interfaces; + +namespace SeaHaven.DataServices.Helpers +{ + public static class WorkOrderBoardProjection + { + public static async Task> ProjectRowsAsync( + IQueryable query, + bool isUnscheduled) + { + var rows = await query + .Select(w => new + { + w.Id, + w.InternalWONumber, + w.RescheduleCount, + w.CarriedOver, + w.WorkOrderType, + w.SiteCode, + LocationName = w.Locations != null ? w.Locations.Name : null, + Poc = w.WorkOrderContacts! + .OrderBy(c => c.Id) + .Select(c => new + { + Name = c.POC != null + ? ((c.POC.FirstName ?? "") + " " + (c.POC.LastName ?? "")).Trim() + : null, + PhoneNumber = c.POC != null ? c.POC.PhoneNumber : null, + c.Notes + }) + .FirstOrDefault(), + w.LifecycleStatus, + w.LegacyStatus, + w.Status, + w.AssignTo, + w.AssignToUser!.FirstName, + w.AssignToUser!.LastName, + w.AssignToUser!.Initials, + w.AssignToUser!.Color, + w.DueDate, + w.ScheduledDate, + w.ScheduledStart, + w.ScheduledEnd, + w.TargetWeek, + w.ScheduleWeekOnly, + VendorId = w.PrimaryDispatch != null ? (int?)w.PrimaryDispatch.VendorId : null, + VendorName = w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null + ? w.PrimaryDispatch.Vendor.CompanyName + : null, + TechName = w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null + ? w.PrimaryDispatch.Vendor.ContactName + : null, + TechPhone = w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null + ? w.PrimaryDispatch.Vendor.Phone + : null, + DispatchApptDate = w.PrimaryDispatch != null ? w.PrimaryDispatch.ScheduledDate : null, + w.Service, + w.Trade, + w.Problem, + w.DocStatus, + w.RowVersion + }) + .ToListAsync(); + + return rows.Select(w => new WorkOrderBoardRawRow( + w.Id, + w.InternalWONumber, + w.RescheduleCount, + w.CarriedOver, + w.WorkOrderType, + w.SiteCode, + w.LocationName, + w.Poc?.Name, + w.Poc?.PhoneNumber, + w.Poc?.Notes, + w.LifecycleStatus, + w.LegacyStatus ?? w.Status, + w.AssignTo, + w.FirstName, + w.LastName, + w.Initials, + w.Color, + w.DueDate, + w.ScheduledDate, + w.ScheduledStart, + w.ScheduledEnd, + w.TargetWeek, + w.ScheduleWeekOnly, + w.VendorId, + w.VendorName, + w.TechName, + w.TechPhone, + w.DispatchApptDate, + w.Service ?? w.Trade ?? w.Problem, + w.DocStatus, + w.RowVersion, + isUnscheduled)).ToList(); + } + } +} diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs new file mode 100644 index 0000000..d13b4d3 --- /dev/null +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs @@ -0,0 +1,166 @@ +using Data.SeaHavenIndustries; +using Data.SeaHavenIndustries.Enums; + +namespace SeaHaven.DataServices.Helpers +{ + public static class WorkOrderBoardQueryFilters + { + public static IQueryable ApplyBaseScope(IQueryable query) + => query.Where(w => w.istemplate != true && (w.IsDeleted != true || w.IsDeleted == null)); + + /// + /// Applies assignee filters. When is true and + /// is set, that takes precedence and + /// is ignored. + /// + public static IQueryable ApplyDispatcherFilter( + IQueryable query, + IReadOnlyList? dispatchers, + bool myWorkOrders, + string? currentUserId) + { + if (myWorkOrders && !string.IsNullOrEmpty(currentUserId)) + return query.Where(w => w.AssignTo == currentUserId); + + if (dispatchers == null || dispatchers.Count == 0) + return query; + + var ids = dispatchers + .Where(d => d != "__unassigned__") + .ToList(); + var includeUnassigned = dispatchers.Contains("__unassigned__"); + + if (includeUnassigned && ids.Count == 0) + return query.Where(w => w.AssignTo == null || w.AssignTo == ""); + + if (includeUnassigned && ids.Count > 0) + { + return query.Where(w => + w.AssignTo == null || w.AssignTo == "" || ids.Contains(w.AssignTo)); + } + + return query.Where(w => w.AssignTo != null && ids.Contains(w.AssignTo)); + } + + public static IQueryable ApplyTypeFilter( + IQueryable query, + IReadOnlyList? types) + { + if (types == null || types.Count == 0) + return query; + + return query.Where(w => w.WorkOrderType != null && types.Contains(w.WorkOrderType.Value)); + } + + #region Phase 2 filter helpers (not wired in Phase 1 board API) + + public static IQueryable ApplySiteFilter( + IQueryable query, + IReadOnlyList? sites) + { + if (sites == null || sites.Count == 0) + return query; + + var normalized = sites + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Select(s => s.Trim()) + .ToList(); + + if (normalized.Count == 0) + return query; + + return query.Where(w => w.SiteCode != null && normalized.Contains(w.SiteCode)); + } + + public static IQueryable ApplyStatusFilter( + IQueryable query, + IReadOnlyList? statuses) + { + if (statuses == null || statuses.Count == 0) + return query; + + return query.Where(w => w.LifecycleStatus != null && statuses.Contains(w.LifecycleStatus.Value)); + } + + public static IQueryable ApplyDocStatusFilter( + IQueryable query, + IReadOnlyList? docStatuses) + { + if (docStatuses == null || docStatuses.Count == 0) + return query; + + return query.Where(w => w.DocStatus != null && docStatuses.Contains(w.DocStatus.Value)); + } + + public static IQueryable ApplyVendorFilter( + IQueryable query, + IReadOnlyList? vendorIds) + { + if (vendorIds == null || vendorIds.Count == 0) + return query; + + return query.Where(w => + w.PrimaryDispatch != null && vendorIds.Contains(w.PrimaryDispatch.VendorId)); + } + + public static IQueryable ApplyPmTypeFilter( + IQueryable query, + IReadOnlyList? pmTypes) + { + if (pmTypes == null || pmTypes.Count == 0) + return query; + + var normalized = pmTypes + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => p.Trim().ToLower()) + .ToList(); + + if (normalized.Count == 0) + return query; + + return query.Where(w => + normalized.Any(pm => + (w.Trade ?? "").ToLower().Contains(pm) + || (w.Problem ?? "").ToLower().Contains(pm))); + } + + public static IQueryable ApplyDateRangeFilter( + IQueryable query, + DateOnly dateFrom, + DateOnly dateTo) + { + var fromDate = dateFrom.ToDateTime(TimeOnly.MinValue).Date; + var toDate = dateTo.ToDateTime(TimeOnly.MinValue).Date; + + return query.Where(w => + (w.ScheduledDate != null + && w.ScheduledDate.Value.Date >= fromDate + && w.ScheduledDate.Value.Date <= toDate) + || (w.ScheduleWeekOnly == true + && w.TargetWeek != null + && w.TargetWeek >= dateFrom + && w.TargetWeek <= dateTo)); + } + + #endregion + + public static IQueryable ApplySearchFilter(IQueryable query, string? search) + { + if (string.IsNullOrWhiteSpace(search)) + return query; + + var s = search.Trim().ToLower(); + return query.Where(w => + (w.SiteCode ?? "").ToLower().Contains(s) + || (w.InternalWONumber ?? "").ToLower().Contains(s) + || (w.WorkerOrderNumber ?? "").ToLower().Contains(s) + || (w.Locations != null && (w.Locations.Name ?? "").ToLower().Contains(s)) + || (w.AssignToUser != null && ((w.AssignToUser.FirstName ?? "") + " " + (w.AssignToUser.LastName ?? "")).ToLower().Contains(s)) + || (w.Service ?? "").ToLower().Contains(s) + || (w.Trade ?? "").ToLower().Contains(s) + || (w.Problem ?? "").ToLower().Contains(s) + || (w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null && (w.PrimaryDispatch.Vendor.CompanyName ?? "").ToLower().Contains(s)) + || (w.Status ?? "").ToLower().Contains(s)); + } + } +} diff --git a/SeaHaven.DataServices/Implementation/WorkOrderBoardDataService.cs b/SeaHaven.DataServices/Implementation/WorkOrderBoardDataService.cs new file mode 100644 index 0000000..be23363 --- /dev/null +++ b/SeaHaven.DataServices/Implementation/WorkOrderBoardDataService.cs @@ -0,0 +1,72 @@ +using Data.SeaHavenIndustries; +using Data.SeaHavenIndustries.Enums; +using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Helpers; +using SeaHaven.DataServices.Interfaces; + +namespace SeaHaven.DataServices.Implementation +{ + public class WorkOrderBoardDataService : IWorkOrderBoardDataService + { + private readonly ApplicationDbContext _context; + + public WorkOrderBoardDataService(ApplicationDbContext context) + { + _context = context; + } + + public async Task GetBoardRowsAsync(WorkOrderBoardQuery query) + { + var weekStartDate = query.WeekStart.ToDateTime(TimeOnly.MinValue); + var weekEndDate = query.WeekEnd.ToDateTime(TimeOnly.MinValue); + + var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking()); + baseQuery = WorkOrderBoardQueryFilters.ApplyDispatcherFilter( + baseQuery, query.Dispatchers, query.MyWorkOrders, query.CurrentUserId); + baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types); + + var scheduledBase = baseQuery.Where(w => + (w.ScheduledDate != null + && w.ScheduledDate.Value.Date >= weekStartDate.Date + && w.ScheduledDate.Value.Date <= weekEndDate.Date) + || (w.ScheduleWeekOnly == true && w.TargetWeek == query.WeekStart)); + + var unscheduledBase = baseQuery.Where(w => + w.ScheduledDate == null + && !LifecycleStatusSets.Terminal.Contains(w.LifecycleStatus)); + + var scheduledTotal = await scheduledBase.CountAsync(); + + if (!string.IsNullOrWhiteSpace(query.Search)) + { + scheduledBase = WorkOrderBoardQueryFilters.ApplySearchFilter(scheduledBase, query.Search); + unscheduledBase = WorkOrderBoardQueryFilters.ApplySearchFilter(unscheduledBase, query.Search); + } + + var scheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync( + scheduledBase.OrderBy(w => w.ScheduledDate).ThenBy(w => w.InternalWONumber), + isUnscheduled: false); + var unscheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync( + unscheduledBase.OrderBy(w => w.InternalWONumber), + isUnscheduled: true); + + return new WorkOrderBoardQueryResult(scheduledRows, unscheduledRows, scheduledTotal); + } + + public async Task> GetDispatcherLookupsAsync() + { + return await _context.Users + .AsNoTracking() + .Where(u => u.IsDeleted != true) + .OrderBy(u => u.FirstName) + .ThenBy(u => u.LastName) + .Select(u => new DispatcherLookupRow( + u.Id, + u.FirstName ?? "", + u.LastName, + u.Initials, + u.Color)) + .ToListAsync(); + } + } +} diff --git a/SeaHaven.DataServices/Interfaces/IWorkOrderBoardDataService.cs b/SeaHaven.DataServices/Interfaces/IWorkOrderBoardDataService.cs new file mode 100644 index 0000000..67f1459 --- /dev/null +++ b/SeaHaven.DataServices/Interfaces/IWorkOrderBoardDataService.cs @@ -0,0 +1,10 @@ +using SeaHaven.DataServices.Interfaces; + +namespace SeaHaven.DataServices.Interfaces +{ + public interface IWorkOrderBoardDataService + { + Task GetBoardRowsAsync(WorkOrderBoardQuery query); + Task> GetDispatcherLookupsAsync(); + } +} diff --git a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs new file mode 100644 index 0000000..44fb01b --- /dev/null +++ b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs @@ -0,0 +1,59 @@ +using Data.SeaHavenIndustries.Enums; + +namespace SeaHaven.DataServices.Interfaces +{ + public record WorkOrderBoardQuery( + DateOnly WeekStart, + DateOnly WeekEnd, + IReadOnlyList? Dispatchers, + bool MyWorkOrders, + IReadOnlyList? Types, + string? Search, + string? CurrentUserId); + + public record WorkOrderBoardRawRow( + int Id, + string? InternalWONumber, + int RescheduleCount, + int CarriedOver, + WorkOrderType? WorkOrderType, + string? SiteCode, + string? LocationName, + string? PocName, + string? PocPhone, + string? PocNotes, + LifecycleStatus? LifecycleStatus, + string? LegacyStatus, + string? AssignTo, + string? DispatcherFirstName, + string? DispatcherLastName, + string? Initials, + string? Color, + DateTime? DueDate, + DateTime? ScheduledDate, + DateTime? ScheduledStart, + DateTime? ScheduledEnd, + DateOnly? TargetWeek, + bool? ScheduleWeekOnly, + int? VendorId, + string? VendorName, + string? TechName, + string? TechPhone, + DateTime? DispatchApptDate, + string? Service, + DocStatus? DocStatus, + byte[]? RowVersion, + bool IsUnscheduled); + + public record WorkOrderBoardQueryResult( + IReadOnlyList ScheduledRows, + IReadOnlyList UnscheduledRows, + int ScheduledTotalBeforeSearch); + + public record DispatcherLookupRow( + string Id, + string FirstName, + string? LastName, + string? Initials, + string? Color); +} diff --git a/SeaHaven.Services/Board/WorkOrderBoardConstants.cs b/SeaHaven.Services/Board/WorkOrderBoardConstants.cs new file mode 100644 index 0000000..78d1ab5 --- /dev/null +++ b/SeaHaven.Services/Board/WorkOrderBoardConstants.cs @@ -0,0 +1,11 @@ +namespace SeaHaven.Services.Board +{ + /// + /// Shared board constants for Phase 2+ features. Not referenced in Phase 1. + /// + public static class WorkOrderBoardConstants + { + public const int MaxWindowDays = 90; + public const int ClientSideThreshold = 300; + } +} diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs new file mode 100644 index 0000000..8e34bfb --- /dev/null +++ b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs @@ -0,0 +1,73 @@ +using Data.SeaHavenIndustries.Enums; + +namespace SeaHaven.Services.DTOs +{ + public class WorkOrderBoardQueryDto + { + public DateOnly WeekStart { get; set; } + public DateOnly? WeekEnd { get; set; } + public List? Dispatchers { get; set; } + public bool MyWorkOrders { get; set; } + public List? Types { get; set; } + public string? Search { get; set; } + } + + public class WorkOrderBoardCountsDto + { + public int Returned { get; set; } + public int Total { get; set; } + } + + public class WorkOrderBoardRowDto + { + public int Id { get; set; } + public string? WoNumber { get; set; } + public int RescheduleCount { get; set; } + public int CarriedOver { get; set; } + public WorkOrderType? WorkOrderType { get; set; } + public bool IsPastDue { get; set; } + public string? SiteCode { get; set; } + public string? LocationName { get; set; } + public string? PocName { get; set; } + public string? PocPhone { get; set; } + public string? PocNotes { get; set; } + public LifecycleStatus? LifecycleStatus { get; set; } + public string? LifecycleStatusLabel { get; set; } + public string? LegacyStatus { get; set; } + public string? DispatcherId { get; set; } + public string? DispatcherName { get; set; } + public string? Initials { get; set; } + public string? Color { get; set; } + public DateTime? DueDate { get; set; } + public DateTime? ScheduledDate { get; set; } + public DateOnly? TargetWeek { get; set; } + public bool? ScheduleWeekOnly { get; set; } + public string? DayGroup { get; set; } + public int? VendorId { get; set; } + public string? VendorName { get; set; } + public string? TechName { get; set; } + public string? TechPhone { get; set; } + public DateTime? ApptDate { get; set; } + public string? ApptTime { get; set; } + public string? Service { get; set; } + public DocStatus? DocStatus { get; set; } + public byte[]? RowVersion { get; set; } + } + + public class WorkOrderBoardResponseDto + { + public DateOnly WeekStart { get; set; } + public DateOnly WeekEnd { get; set; } + public WorkOrderBoardCountsDto Counts { get; set; } = new(); + public List Unscheduled { get; set; } = new(); + public List Scheduled { get; set; } = new(); + } + + public class DispatcherLookupDto + { + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + public string? Initials { get; set; } + public string? Color { get; set; } + } +} diff --git a/SeaHaven.Services/Helpers/WorkOrderBoardApptTimeParser.cs b/SeaHaven.Services/Helpers/WorkOrderBoardApptTimeParser.cs new file mode 100644 index 0000000..f50949a --- /dev/null +++ b/SeaHaven.Services/Helpers/WorkOrderBoardApptTimeParser.cs @@ -0,0 +1,57 @@ +namespace SeaHaven.Services.Helpers +{ + public static class WorkOrderBoardApptTimeParser + { + public static bool TryParse(string? value, out TimeSpan? start, out TimeSpan? end, out string? error) + { + start = null; + end = null; + error = null; + + if (string.IsNullOrWhiteSpace(value)) + return true; + + var trimmed = value.Trim(); + var parts = trimmed.Split(new[] { '–', '—', '-' }) + .Select(p => p.Trim()) + .Where(p => !string.IsNullOrEmpty(p)) + .ToArray(); + + if (parts.Length == 1) + { + if (!TimeSpan.TryParse(parts[0], out var single)) + { + error = "Invalid appointment time format."; + return false; + } + + start = single; + return true; + } + + if (parts.Length == 2) + { + if (!TimeSpan.TryParse(parts[0], out var startTime) || !TimeSpan.TryParse(parts[1], out var endTime)) + { + error = "Invalid appointment time range format."; + return false; + } + + start = startTime; + end = endTime; + return true; + } + + error = "Invalid appointment time format."; + return false; + } + + public static DateTime? CombineDateAndTime(DateTime? date, TimeSpan? time) + { + if (!date.HasValue || !time.HasValue) + return date; + + return date.Value.Date.Add(time.Value); + } + } +} diff --git a/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs b/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs index 873126d..b64d16c 100644 --- a/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs +++ b/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs @@ -1,55 +1,107 @@ -using Data.SeaHavenIndustries; -using Data.SeaHavenIndustries.Enums; - -namespace SeaHaven.Services.Helpers -{ - public static class WorkOrderDerivedFields - { - /// - /// Past due overlay — never a WorkOrderType. Matches SHOC board design. - /// - 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; - } - - /// - /// Sets initial lifecycle on create. New WOs start Incomplete unless auto-scheduled. - /// - public static void SetInitialLifecycleStatus(WorkOrder workOrder) - { - workOrder.LifecycleStatus ??= LifecycleStatus.Incomplete; - ApplyAutoScheduleIfEligible(workOrder); - } - - /// - /// SHOC rule: specific scheduled date + assignee + Incomplete → Scheduled. - /// Week-only targets do not auto-schedule. - /// - 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; - } - } -} - \ No newline at end of file +using Data.SeaHavenIndustries; +using Data.SeaHavenIndustries.Enums; + +namespace SeaHaven.Services.Helpers +{ + public static class WorkOrderDerivedFields + { + /// + /// Past due overlay — never a WorkOrderType. Matches SHOC board design. + /// + 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; + } + + /// + /// Board read-model past-due check. Phase 1 compares UTC calendar dates; + /// business/location timezone support is planned for a later phase. + /// + public static bool IsPastDue( + DateTime? scheduledDate, + LifecycleStatus? lifecycleStatus, + DateTime? utcNow = null) + { + if (!scheduledDate.HasValue) + return false; + + if (LifecycleStatusMapper.IsTerminal(lifecycleStatus)) + return false; + + var today = (utcNow ?? DateTime.UtcNow).Date; + return scheduledDate.Value.Date < today; + } + + /// + /// Sets initial lifecycle on create. New WOs start Incomplete unless auto-scheduled. + /// + public static void SetInitialLifecycleStatus(WorkOrder workOrder) + { + workOrder.LifecycleStatus ??= LifecycleStatus.Incomplete; + ApplyAutoScheduleIfEligible(workOrder); + } + + /// + /// SHOC rule: specific scheduled date + assignee + Incomplete → Scheduled. + /// Week-only targets do not auto-schedule. + /// + 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; + } + + /// + /// Maps scheduled date to Mon–Fri board column. Phase 1 uses the stored + /// date's day-of-week (UTC); timezone-aware grouping is planned for a later phase. + /// + public static string? GetDayGroup(DateTime? scheduledDate) + { + if (!scheduledDate.HasValue) + return null; + + return scheduledDate.Value.DayOfWeek switch + { + DayOfWeek.Monday => "monday", + DayOfWeek.Tuesday => "tuesday", + DayOfWeek.Wednesday => "wednesday", + DayOfWeek.Thursday => "thursday", + DayOfWeek.Friday => "friday", + _ => null + }; + } + + public static string? FormatApptTime(DateTime? start, DateTime? end) + { + if (!start.HasValue) + return null; + + if (end.HasValue) + return $"{start.Value:HH:mm} – {end.Value:HH:mm}"; + + return start.Value.ToString("HH:mm"); + } + + public static string GetLifecycleStatusLabel(LifecycleStatus? status) + => status.HasValue ? LifecycleStatusMapper.ToFeLabel(status.Value) : ""; + } +} diff --git a/SeaHaven.Services/Helpers/WorkOrderOperationalWeek.cs b/SeaHaven.Services/Helpers/WorkOrderOperationalWeek.cs new file mode 100644 index 0000000..e8795f1 --- /dev/null +++ b/SeaHaven.Services/Helpers/WorkOrderOperationalWeek.cs @@ -0,0 +1,35 @@ +namespace SeaHaven.Services.Helpers +{ + public static class WorkOrderOperationalWeek + { + public static DateOnly GetMondayOfWeek(DateOnly date) + { + var daysFromMonday = ((int)date.DayOfWeek + 6) % 7; + return date.AddDays(-daysFromMonday); + } + + public static DateOnly GetPreviousOperationalWeekStart(DateTime utcNow) + => GetMondayOfWeek(DateOnly.FromDateTime(utcNow)).AddDays(-7); + + public static DateOnly GetOperationalWeekEnd(DateOnly weekStartMonday) + => weekStartMonday.AddDays(4); + + public static string BuildWeekCorrelationId(DateOnly sourceWeekStart) + => $"week:{sourceWeekStart:yyyy-MM-dd}"; + + /// + /// Returns a validation error message when the week window is invalid; otherwise null. + /// + public static string? ValidateWeekWindow(DateOnly weekStart, DateOnly weekEnd) + { + if (weekEnd < weekStart) + return "weekEnd must be on or after weekStart."; + + var spanDays = weekEnd.DayNumber - weekStart.DayNumber; + if (spanDays > 6) + return "Week window cannot exceed 7 days."; + + return null; + } + } +} diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs new file mode 100644 index 0000000..03b5b56 --- /dev/null +++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs @@ -0,0 +1,114 @@ +using Data.SeaHavenIndustries.Enums; +using SeaHaven.DataServices.Interfaces; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Helpers; +using SeaHaven.Services.Interfaces; + +namespace SeaHaven.Services.Implementation +{ + public class WorkOrderBoardService : IWorkOrderBoardService + { + private readonly IWorkOrderBoardDataService _boardDataService; + + public WorkOrderBoardService(IWorkOrderBoardDataService boardDataService) + { + _boardDataService = boardDataService; + } + + public async Task GetBoardAsync( + WorkOrderBoardQueryDto query, + string? currentUserId) + { + var weekStart = query.WeekStart; + var weekEnd = query.WeekEnd ?? weekStart.AddDays(4); + + var validationError = WorkOrderOperationalWeek.ValidateWeekWindow(weekStart, weekEnd); + if (validationError != null) + throw new ArgumentException(validationError); + + var dataQuery = new WorkOrderBoardQuery( + weekStart, + weekEnd, + query.Dispatchers, + query.MyWorkOrders, + query.Types, + query.Search, + currentUserId); + + var result = await _boardDataService.GetBoardRowsAsync(dataQuery); + var utcNow = DateTime.UtcNow; + + return new WorkOrderBoardResponseDto + { + WeekStart = weekStart, + WeekEnd = weekEnd, + Counts = new WorkOrderBoardCountsDto + { + Total = result.ScheduledTotalBeforeSearch, + Returned = result.ScheduledRows.Count + }, + Scheduled = result.ScheduledRows.Select(r => MapRow(r, utcNow)).ToList(), + Unscheduled = result.UnscheduledRows.Select(r => MapRow(r, utcNow)).ToList() + }; + } + + public async Task> GetDispatcherLookupsAsync() + { + var rows = await _boardDataService.GetDispatcherLookupsAsync(); + return rows.Select(r => new DispatcherLookupDto + { + Id = r.Id, + Name = FormatName(r.FirstName, r.LastName), + Initials = r.Initials, + Color = r.Color + }).ToList(); + } + + private static WorkOrderBoardRowDto MapRow(WorkOrderBoardRawRow row, DateTime utcNow) + { + // Appt column: date prefers dispatch appointment (vendor slot), then WO ScheduledDate. + // Time prefers WO ScheduledStart/End (dispatcher window), then dispatch datetime. + var apptStart = row.ScheduledStart ?? row.DispatchApptDate; + var apptEnd = row.ScheduledEnd; + + return new WorkOrderBoardRowDto + { + Id = row.Id, + WoNumber = row.InternalWONumber, + RescheduleCount = row.RescheduleCount, + CarriedOver = row.CarriedOver, + WorkOrderType = row.WorkOrderType, + IsPastDue = WorkOrderDerivedFields.IsPastDue(row.ScheduledDate, row.LifecycleStatus, utcNow), + SiteCode = row.SiteCode, + LocationName = row.LocationName, + PocName = row.PocName, + PocPhone = row.PocPhone, + PocNotes = row.PocNotes, + LifecycleStatus = row.LifecycleStatus, + LifecycleStatusLabel = WorkOrderDerivedFields.GetLifecycleStatusLabel(row.LifecycleStatus), + LegacyStatus = row.LegacyStatus, + DispatcherId = row.AssignTo, + DispatcherName = FormatName(row.DispatcherFirstName, row.DispatcherLastName), + Initials = row.Initials, + Color = row.Color, + DueDate = row.DueDate, + ScheduledDate = row.ScheduledDate, + TargetWeek = row.TargetWeek, + ScheduleWeekOnly = row.ScheduleWeekOnly, + DayGroup = WorkOrderDerivedFields.GetDayGroup(row.ScheduledDate), + VendorId = row.VendorId, + VendorName = row.VendorName, + TechName = row.TechName, + TechPhone = row.TechPhone, + ApptDate = row.DispatchApptDate ?? row.ScheduledDate, + ApptTime = WorkOrderDerivedFields.FormatApptTime(apptStart, apptEnd), + Service = row.Service, + DocStatus = row.DocStatus, + RowVersion = row.RowVersion + }; + } + + private static string FormatName(string? firstName, string? lastName) + => $"{firstName ?? ""} {lastName ?? ""}".Trim(); + } +} diff --git a/SeaHaven.Services/Interfaces/IWorkOrderBoardService.cs b/SeaHaven.Services/Interfaces/IWorkOrderBoardService.cs new file mode 100644 index 0000000..d24e64a --- /dev/null +++ b/SeaHaven.Services/Interfaces/IWorkOrderBoardService.cs @@ -0,0 +1,10 @@ +using SeaHaven.Services.DTOs; + +namespace SeaHaven.Services.Interfaces +{ + public interface IWorkOrderBoardService + { + Task GetBoardAsync(WorkOrderBoardQueryDto query, string? currentUserId); + Task> GetDispatcherLookupsAsync(); + } +} diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs new file mode 100644 index 0000000..8b1b867 --- /dev/null +++ b/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs @@ -0,0 +1,210 @@ +using Data.SeaHavenIndustries; +using Data.SeaHavenIndustries.Enums; +using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Implementation; +using SeaHaven.DataServices.Interfaces; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Implementation; + +namespace SeaHavenIndustries.Tests; + +public class WorkOrderBoardDataServiceTests +{ + private static ApplicationDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new ApplicationDbContext(options); + } + + [Fact] + public async Task GetBoardRows_ReturnsScheduledInWeekAndUnscheduled() + { + await using var context = CreateContext(); + var weekStart = new DateOnly(2026, 6, 22); + + context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC", ContactName = "Tech", Phone = "555-0100" }); + context.Dispatches.Add(new Dispatch + { + Id = 10, + VendorId = 1, + ScheduledDate = new DateTime(2026, 6, 24, 8, 0, 0) + }); + + context.workOrders.AddRange( + new WorkOrder + { + Id = 1, + InternalWONumber = "10000000001", + ScheduledDate = new DateTime(2026, 6, 23), + LifecycleStatus = LifecycleStatus.Scheduled, + PrimaryDispatchId = 10, + SiteCode = "BK5" + }, + new WorkOrder + { + Id = 2, + InternalWONumber = "10000000002", + ScheduledDate = null, + LifecycleStatus = LifecycleStatus.Incomplete, + SiteCode = "BK6" + }, + new WorkOrder + { + Id = 3, + InternalWONumber = "10000000003", + ScheduledDate = new DateTime(2026, 7, 1), + LifecycleStatus = LifecycleStatus.Scheduled + }); + + await context.SaveChangesAsync(); + + var service = new WorkOrderBoardDataService(context); + var result = await service.GetBoardRowsAsync(new WorkOrderBoardQuery( + weekStart, + weekStart.AddDays(4), + null, + false, + null, + null, + null)); + + Assert.Single(result.ScheduledRows); + Assert.Equal(1, result.ScheduledRows[0].Id); + Assert.Equal("Acme HVAC", result.ScheduledRows[0].VendorName); + Assert.Single(result.UnscheduledRows); + Assert.Equal(2, result.UnscheduledRows[0].Id); + Assert.Equal(1, result.ScheduledTotalBeforeSearch); + } + + [Fact] + public async Task GetBoardRows_FiltersByDispatcher() + { + await using var context = CreateContext(); + var weekStart = new DateOnly(2026, 6, 22); + + context.workOrders.AddRange( + new WorkOrder + { + Id = 1, + AssignTo = "user-a", + ScheduledDate = new DateTime(2026, 6, 23), + LifecycleStatus = LifecycleStatus.Scheduled + }, + new WorkOrder + { + Id = 2, + AssignTo = "user-b", + ScheduledDate = new DateTime(2026, 6, 24), + LifecycleStatus = LifecycleStatus.Scheduled + }); + + await context.SaveChangesAsync(); + + var service = new WorkOrderBoardDataService(context); + var result = await service.GetBoardRowsAsync(new WorkOrderBoardQuery( + weekStart, + weekStart.AddDays(4), + new[] { "user-a" }, + false, + null, + null, + null)); + + Assert.Single(result.ScheduledRows); + Assert.Equal("user-a", result.ScheduledRows[0].AssignTo); + } + + [Fact] + public async Task GetBoardRows_MyWorkOrders_FiltersCurrentUser() + { + await using var context = CreateContext(); + var weekStart = new DateOnly(2026, 6, 22); + + context.workOrders.AddRange( + new WorkOrder + { + Id = 1, + AssignTo = "me", + ScheduledDate = new DateTime(2026, 6, 23), + LifecycleStatus = LifecycleStatus.Scheduled + }, + new WorkOrder + { + Id = 2, + AssignTo = "other", + ScheduledDate = new DateTime(2026, 6, 23), + LifecycleStatus = LifecycleStatus.Scheduled + }); + + await context.SaveChangesAsync(); + + var service = new WorkOrderBoardDataService(context); + var result = await service.GetBoardRowsAsync(new WorkOrderBoardQuery( + weekStart, + weekStart.AddDays(4), + null, + true, + null, + null, + "me")); + + Assert.Single(result.ScheduledRows); + Assert.Equal(1, result.ScheduledRows[0].Id); + } +} + +public class WorkOrderBoardServiceTests +{ + private static ApplicationDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new ApplicationDbContext(options); + } + + [Fact] + public async Task GetBoardAsync_ComputesCountsAndPastDue() + { + await using var context = CreateContext(); + var weekStart = new DateOnly(2026, 6, 22); + + context.workOrders.Add(new WorkOrder + { + Id = 1, + InternalWONumber = "10000000001", + ScheduledDate = new DateTime(2026, 6, 24, 12, 0, 0, DateTimeKind.Utc), + LifecycleStatus = LifecycleStatus.Scheduled + }); + + await context.SaveChangesAsync(); + + var dataService = new WorkOrderBoardDataService(context); + var boardService = new WorkOrderBoardService(dataService); + + var response = await boardService.GetBoardAsync(new WorkOrderBoardQueryDto + { + WeekStart = weekStart + }, null); + + Assert.Equal(1, response.Counts.Total); + Assert.Single(response.Scheduled); + Assert.True(response.Scheduled[0].IsPastDue); + Assert.Equal("wednesday", response.Scheduled[0].DayGroup); + } + + [Fact] + public async Task GetBoardAsync_InvalidWeekWindow_Throws() + { + await using var context = CreateContext(); + var boardService = new WorkOrderBoardService(new WorkOrderBoardDataService(context)); + + await Assert.ThrowsAsync(() => boardService.GetBoardAsync(new WorkOrderBoardQueryDto + { + WeekStart = new DateOnly(2026, 6, 22), + WeekEnd = new DateOnly(2026, 6, 15) + }, null)); + } +} diff --git a/docs/work-orders/phase-1/README.md b/docs/work-orders/phase-1/README.md new file mode 100644 index 0000000..660bfb3 --- /dev/null +++ b/docs/work-orders/phase-1/README.md @@ -0,0 +1,159 @@ +# Fase 1 — Weekly Board (leitura) + +**Programa:** Work Orders Board +**Objetivo:** Endpoint window-based para carregar a visão semanal do SHOC (Mon–Fri + Unscheduled) com projeção das 14 colunas operacionais. + +--- + +## Endpoints + +### `GET /api/workorders/board` + +Retorna WOs agendadas na semana + seção **Unscheduled** em uma única chamada. + +**Autenticação:** Bearer JWT (`[Authorize]`) + +**Query params:** + +| Param | Obrigatório | Descrição | +|-------|-------------|-----------| +| `weekStart` | Sim (`BindRequired`) | Segunda-feira da semana (`YYYY-MM-DD`). Omitir retorna **400**. | +| `weekEnd` | Não | Default: `weekStart + 4 dias` (sexta). Janela pode ter até 7 dias. | +| `dispatchers` | Não | Lista de GUIDs; use `__unassigned__` para não atribuídos. **Ignorado** quando `myWorkOrders=true`. | +| `myWorkOrders` | Não | `true` → filtra `AssignTo == usuário logado` e tem **precedência** sobre `dispatchers` (não é AND). | +| `types` | Não | Valores enum `WorkOrderType` (`PM`, `PO`, `Emergency`, etc.) | +| `search` | Não | Busca contextual (site, WO#, location, dispatcher, trade, vendor, status) | + +**Exemplo:** + +```bash +curl -H "Authorization: Bearer " \ + "https://localhost:5001/api/workorders/board?weekStart=2026-06-22&myWorkOrders=true" +``` + +**Response:** + +```json +{ + "weekStart": "2026-06-22", + "weekEnd": "2026-06-26", + "counts": { "returned": 42, "total": 58 }, + "unscheduled": [ /* WorkOrderBoardRowDto[] */ ], + "scheduled": [ /* WorkOrderBoardRowDto[] — agrupar por dayGroup no FE */ ] +} +``` + +- `counts.total` — WOs agendadas na semana (filtros dispatcher/tipo, **sem** search) +- `counts.returned` — WOs agendadas após aplicar search +- `dayGroup` — `monday`…`friday` para dias úteis; **`null` no sábado/domingo** (a janela pode incluir fim de semana; o FE deve tratar `dayGroup` nulo) +- Referência de dia / past-due: **UTC** nesta fase (timezone de negócio fica para fase posterior) + +### `GET /api/workorders/lookups/dispatchers` + +Lista opções para o filtro/dropdown de assignee do board. + +**Escopo atual (placeholder):** todos os users com `IsDeleted != true` — ainda **sem** filtro por role. Quando roles de assignee estiverem definidos, este lookup será restrito. + +```json +[ + { "id": "guid", "name": "Jane Doe", "initials": "JD", "color": "#4A90D9" } +] +``` + +--- + +## Matriz coluna → campo API + +| Coluna board | Campos response | +|--------------|-----------------| +| WO# | `woNumber`, `rescheduleCount`, `carriedOver` | +| Type | `workOrderType`, `isPastDue` | +| Site | `siteCode`, `locationName`, `pocName`, `pocPhone`, `pocNotes` | +| Status | `lifecycleStatus`, `lifecycleStatusLabel`, `legacyStatus` | +| Assignee | `dispatcherId`, `dispatcherName`, `initials`, `color` | +| Due | `dueDate` | +| Scheduled | `scheduledDate`, `targetWeek`, `scheduleWeekOnly`, `dayGroup` | +| Vendor | `vendorId`, `vendorName`, `techName`, `techPhone` | +| Appt | `apptDate`, `apptTime` | +| Past Due | `isPastDue` (derivado on-read) | +| Carried | `carriedOver` | +| Reschedule | `rescheduleCount` | +| Doc | `docStatus` | +| Actions | FE only | + +--- + +## Regras de derivação + +### isPastDue + +``` +ScheduledDate < UTC hoje +AND LifecycleStatus NOT IN (Complete, Canceled, Closed) +``` + +Implementação: `SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs` + +### Vendor / Appt + +Projeção read-only via `WorkOrder.PrimaryDispatchId` → `Dispatch` → `Vendor` ([ADR](../phase-0/adr-vendor-source-of-truth.md)). + +### Janela semanal + +WO entra em **scheduled** se: + +- `ScheduledDate` entre `weekStart` e `weekEnd`, **ou** +- `ScheduleWeekOnly == true` e `TargetWeek == weekStart` + +**Unscheduled:** `ScheduledDate == null` e status não terminal (`LifecycleStatusSets.Terminal`). + +--- + +## Código entregue + +| Camada | Arquivo | +|--------|---------| +| DTOs | `SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs` | +| Derived fields | `SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs` | +| Data | `SeaHaven.DataServices/Implementation/WorkOrderBoardDataService.cs` | +| Service | `SeaHaven.Services/Implementation/WorkOrderBoardService.cs` | +| API | `Api.SeaHavenIndustries/Controllers/WorkOrderController.cs` (`board`, `lookups/dispatchers`) | +| Migration | `Data.SeaHavenIndustries/Migrations/20260701120000_Phase1_BoardIndexes.cs` | +| Testes | `SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs`, `WorkOrderBoardServiceTests.cs` | + +--- + +## Gates de aceite + +### Desenvolvimento (concluído) + +- [x] Endpoint `GET board` — semana + Unscheduled +- [x] Projeção 12/14 colunas de dados +- [x] `isPastDue` derivado on-read +- [x] Vendor via dispatch primário +- [x] Filtros dispatcher / My WOs / tipo / search +- [x] Contador X of Y +- [x] Índices Tier S +- [x] 14 testes unitários board (+ 12 Fase 0 = 26 total) + +### Staging / produção (pendente) + +- [ ] Sign-off CTO/PO (herda gates Fase 0) +- [ ] Dry-run migration Phase0 + Phase1 em staging +- [ ] Tier volume confirmado em staging/prod +- [ ] Smoke checklist Blazor / Sync / Portal +- [ ] Gate Vendor Portal regression (endpoints dispatch inalterados) +- [ ] Latência aceitável com volume real (Tier S: < 25k WOs) +- [ ] Feature flag SHOC board em staging + +--- + +## Coexistência + +Endpoints legados (`GetWorkOrderList`, `GetWorkorderById`, etc.) **não foram alterados**. O board usa contrato novo em rotas separadas. + +--- + +## Próximo passo + +**Fase 2** — Inline edit + concorrência (dual RowVersion, audit por campo, PATCH granular).