mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 16:19:07 +00:00
Merge 4e32c487d2 into b2aeb35a55
This commit is contained in:
commit
cb3b781d59
192 changed files with 26564 additions and 257 deletions
17
.env.example
17
.env.example
|
|
@ -1,11 +1,11 @@
|
|||
# Sea Haven Industries — shoc-backend required configuration
|
||||
# Sea Haven Industries ÔÇö shoc-backend required configuration
|
||||
#
|
||||
# This file documents the secrets that used to be hardcoded in appsettings*.json
|
||||
# and source files. Copy the values into one of the supported configuration sources;
|
||||
# do NOT commit real values.
|
||||
#
|
||||
# .NET resolves configuration in this order (later wins):
|
||||
# 1. appsettings.json / appsettings.{Environment}.json (committed — placeholders only)
|
||||
# 1. appsettings.json / appsettings.{Environment}.json (committed ÔÇö placeholders only)
|
||||
# 2. User Secrets (local dev): dotnet user-secrets set "Key:Sub" "value"
|
||||
# 3. Environment variables (use "__" as the section separator)
|
||||
#
|
||||
|
|
@ -32,3 +32,16 @@ GoogleMaps__ApiKey=AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|||
# AWS_ACCESS_KEY_ID=
|
||||
# AWS_SECRET_ACCESS_KEY=
|
||||
# AWS_REGION=us-east-2
|
||||
|
||||
# --- Work Order ingest (Lambda cutover) ---
|
||||
# Leave Enabled=false until ApiKey is a real secret (min 32 chars). Do not use appsettings placeholders.
|
||||
WorkOrderIngest__Enabled=false
|
||||
WorkOrderIngest__ApiKey=<shared-secret-for-X-Ingest-Key-min-32-chars>
|
||||
|
||||
# --- Dynamo sync bridge (keep false after Lambda cutover; avoid dual-run with ingest) ---
|
||||
Sync__Enabled=true
|
||||
|
||||
# --- Legacy endpoint deprecation headers ---
|
||||
LegacyEndpoints__DeprecationEnabled=false
|
||||
LegacyEndpoints__SunsetDate=2026-12-31
|
||||
|
||||
|
|
|
|||
109
Api.SeaHavenIndustries.Tests/LocationControllerSitesTests.cs
Normal file
109
Api.SeaHavenIndustries.Tests/LocationControllerSitesTests.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
using Api.SeaHavenIndustries.Controllers;
|
||||
using Data.SeaHavenIndustries;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
using SeaHaven.DataServices.Implementation;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Implementation;
|
||||
using SeaHaven.Services.Validation;
|
||||
using Xunit;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Tests;
|
||||
|
||||
public class LocationControllerSitesTests
|
||||
{
|
||||
private static ApplicationDbContext NewContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new ApplicationDbContext(options);
|
||||
}
|
||||
|
||||
private static LocationController NewController(ApplicationDbContext ctx)
|
||||
{
|
||||
var dataService = new LocationDataService(ctx);
|
||||
var service = new LocationService(
|
||||
dataService,
|
||||
Mock.Of<ICreateLocationValidation>(),
|
||||
Mock.Of<IUpdateLocationValidation>());
|
||||
|
||||
var controller = new LocationController(service, dataService)
|
||||
{
|
||||
ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext()
|
||||
}
|
||||
};
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static async Task SeedSitesAsync(ApplicationDbContext ctx)
|
||||
{
|
||||
ctx.Locations.AddRange(
|
||||
new Locations { Id = 1, Name = "STL8", City = "St. Louis", State = "MO" },
|
||||
new Locations { Id = 2, Name = "BK5", City = "Dallas", State = "TX" },
|
||||
new Locations { Id = 3, Name = "IND9", City = "Indianapolis", State = "IN" },
|
||||
new Locations { Id = 4, Name = "DFW8", City = "Dallas", State = "TX" });
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSites_ReturnsIdCodeCityState_OrderedByCode()
|
||||
{
|
||||
await using var ctx = NewContext();
|
||||
await SeedSitesAsync(ctx);
|
||||
var controller = NewController(ctx);
|
||||
|
||||
var result = await controller.GetSites();
|
||||
|
||||
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||
var data = ok.Value.Should().BeAssignableTo<IReadOnlyList<SiteOptionDTO>>().Subject;
|
||||
|
||||
data.Should().HaveCount(4);
|
||||
data.Select(s => s.Code).Should().ContainInOrder("BK5", "DFW8", "IND9", "STL8");
|
||||
data[0].Should().BeEquivalentTo(new SiteOptionDTO
|
||||
{
|
||||
Id = 2,
|
||||
Code = "BK5",
|
||||
City = "Dallas",
|
||||
State = "TX"
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSites_FiltersByCode()
|
||||
{
|
||||
await using var ctx = NewContext();
|
||||
await SeedSitesAsync(ctx);
|
||||
var controller = NewController(ctx);
|
||||
|
||||
var result = await controller.GetSites("bk5");
|
||||
|
||||
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||
var data = ok.Value.Should().BeAssignableTo<IReadOnlyList<SiteOptionDTO>>().Subject;
|
||||
|
||||
data.Should().ContainSingle();
|
||||
data[0].Code.Should().Be("BK5");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSites_FiltersByCity()
|
||||
{
|
||||
await using var ctx = NewContext();
|
||||
await SeedSitesAsync(ctx);
|
||||
var controller = NewController(ctx);
|
||||
|
||||
var result = await controller.GetSites("Dallas");
|
||||
|
||||
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||
var data = ok.Value.Should().BeAssignableTo<IReadOnlyList<SiteOptionDTO>>().Subject;
|
||||
|
||||
data.Should().HaveCount(2);
|
||||
data.Select(s => s.Code).Should().BeEquivalentTo("BK5", "DFW8");
|
||||
}
|
||||
}
|
||||
163
Api.SeaHavenIndustries.Tests/WorkOrderControllerSearchTests.cs
Normal file
163
Api.SeaHavenIndustries.Tests/WorkOrderControllerSearchTests.cs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
using Api.SeaHavenIndustries.Controllers;
|
||||
using Api.SeaHavenIndustries.DTOs;
|
||||
using Data.SeaHavenIndustries;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Moq;
|
||||
using SeaHaven.DataServices.Implementation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Helpers;
|
||||
using SeaHaven.Services.Implementation;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using Xunit;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Tests;
|
||||
|
||||
public class WorkOrderControllerSearchTests
|
||||
{
|
||||
private static ApplicationDbContext NewContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new ApplicationDbContext(options);
|
||||
}
|
||||
|
||||
private static UserManager<ApplicationUser> MockUserManager()
|
||||
{
|
||||
var store = new Mock<IUserStore<ApplicationUser>>();
|
||||
return new Mock<UserManager<ApplicationUser>>(
|
||||
store.Object, null!, null!, null!, null!, null!, null!, null!, null!).Object;
|
||||
}
|
||||
|
||||
private static WorkOrderController NewController(
|
||||
ApplicationDbContext ctx,
|
||||
IWorkOrderAdvancedSearchService advancedSearch)
|
||||
{
|
||||
var controller = new WorkOrderController(
|
||||
MockUserManager(),
|
||||
Mock.Of<IWorkOrderService>(),
|
||||
Mock.Of<IWorkOrderDataService>(),
|
||||
Mock.Of<ICommentDataService>(),
|
||||
Mock.Of<IQuotesDataService>(),
|
||||
Mock.Of<IUserDataService>(),
|
||||
Mock.Of<IDispatchDataService>(),
|
||||
Mock.Of<IVendorDataService>(),
|
||||
Mock.Of<ITaskListTemplateDataService>(),
|
||||
ctx,
|
||||
Mock.Of<IWebHostEnvironment>(),
|
||||
Mock.Of<IHttpContextAccessor>(),
|
||||
null!,
|
||||
null!,
|
||||
Mock.Of<IConfiguration>(),
|
||||
Mock.Of<IWorkOrderBoardService>(),
|
||||
Mock.Of<IWorkOrderBoardUpdateService>(),
|
||||
Mock.Of<IWorkOrderBoardCreateService>(),
|
||||
Mock.Of<IWorkOrderBoardCancelService>(),
|
||||
advancedSearch,
|
||||
Mock.Of<IWorkOrderDetailService>(),
|
||||
Mock.Of<IWorkOrderCommentService>(),
|
||||
Mock.Of<IWorkOrderCompletionService>(),
|
||||
Mock.Of<IWorkOrderMediaService>());
|
||||
|
||||
controller.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext
|
||||
{
|
||||
User = new System.Security.Claims.ClaimsPrincipal(
|
||||
new System.Security.Claims.ClaimsIdentity(
|
||||
new[]
|
||||
{
|
||||
new System.Security.Claims.Claim(
|
||||
System.Security.Claims.ClaimTypes.NameIdentifier,
|
||||
"test-user")
|
||||
},
|
||||
"Test"))
|
||||
}
|
||||
};
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchBoard_WhenCustomPresetMissingDates_ReturnsBadRequest()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
var advancedSearch = new WorkOrderAdvancedSearchService(
|
||||
Mock.Of<IWorkOrderAdvancedSearchDataService>());
|
||||
var controller = NewController(ctx, advancedSearch);
|
||||
|
||||
var result = await controller.SearchBoard(new WorkOrderAdvancedSearchQueryDto
|
||||
{
|
||||
DatePreset = WorkOrderAdvancedSearchDatePreset.Custom
|
||||
});
|
||||
|
||||
var badRequest = result.Should().BeOfType<BadRequestObjectResult>().Subject;
|
||||
var body = badRequest.Value.Should().BeOfType<Response>().Subject;
|
||||
body.Status.Should().Be("Error");
|
||||
body.Message.Should().Contain("dateFrom");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBoard_WhenWeekEndBeforeWeekStart_ReturnsBadRequest()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
var boardService = new WorkOrderBoardService(new WorkOrderBoardDataService(ctx));
|
||||
var controller = NewController(ctx, Mock.Of<IWorkOrderAdvancedSearchService>());
|
||||
// Replace board service - controller uses injected mock. Need different approach.
|
||||
|
||||
// Use real board service via manual controller setup
|
||||
var advancedSearch = Mock.Of<IWorkOrderAdvancedSearchService>();
|
||||
var controllerWithBoard = new WorkOrderController(
|
||||
MockUserManager(),
|
||||
Mock.Of<IWorkOrderService>(),
|
||||
Mock.Of<IWorkOrderDataService>(),
|
||||
Mock.Of<ICommentDataService>(),
|
||||
Mock.Of<IQuotesDataService>(),
|
||||
Mock.Of<IUserDataService>(),
|
||||
Mock.Of<IDispatchDataService>(),
|
||||
Mock.Of<IVendorDataService>(),
|
||||
Mock.Of<ITaskListTemplateDataService>(),
|
||||
ctx,
|
||||
Mock.Of<IWebHostEnvironment>(),
|
||||
Mock.Of<IHttpContextAccessor>(),
|
||||
null!,
|
||||
null!,
|
||||
Mock.Of<IConfiguration>(),
|
||||
boardService,
|
||||
Mock.Of<IWorkOrderBoardUpdateService>(),
|
||||
Mock.Of<IWorkOrderBoardCreateService>(),
|
||||
Mock.Of<IWorkOrderBoardCancelService>(),
|
||||
advancedSearch,
|
||||
Mock.Of<IWorkOrderDetailService>(),
|
||||
Mock.Of<IWorkOrderCommentService>(),
|
||||
Mock.Of<IWorkOrderCompletionService>(),
|
||||
Mock.Of<IWorkOrderMediaService>());
|
||||
|
||||
controllerWithBoard.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext
|
||||
{
|
||||
User = new System.Security.Claims.ClaimsPrincipal(
|
||||
new System.Security.Claims.ClaimsIdentity(
|
||||
new[] { new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, "test-user") },
|
||||
"Test"))
|
||||
}
|
||||
};
|
||||
|
||||
var result = await controllerWithBoard.GetBoard(
|
||||
new DateOnly(2026, 6, 22),
|
||||
new DateOnly(2026, 6, 15));
|
||||
|
||||
var badRequest = result.Should().BeOfType<BadRequestObjectResult>().Subject;
|
||||
var body = badRequest.Value.Should().BeOfType<Response>().Subject;
|
||||
body.Status.Should().Be("Error");
|
||||
body.Message.Should().Contain("weekEnd");
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,13 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
_locationDataService = locationDataService;
|
||||
}
|
||||
|
||||
[HttpGet("sites")]
|
||||
public async Task<IActionResult> GetSites(string? search = null)
|
||||
{
|
||||
var data = await _locationService.GetSiteOptionsAsync(search);
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
[HttpGet("GetLocationList")]
|
||||
public async Task<IActionResult> GetLocationList(string? search = "", int page = 1, int pageSize = 10)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,32 +1,49 @@
|
|||
using Amazon.DynamoDBv2;
|
||||
using Amazon.DynamoDBv2.Model;
|
||||
using Api.SeaHavenIndustries.Options;
|
||||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Authorize(Roles = "Admin")]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class SyncController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _db;
|
||||
private readonly AmazonDynamoDBClient _dynamo;
|
||||
private readonly SyncOptions _syncOptions;
|
||||
private const string WO_TABLE = "WorkOrders";
|
||||
private const string COMMENTS_TABLE = "WorkOrderComments";
|
||||
private const string VENDOR_REPLIES_TABLE = "VendorReplies";
|
||||
|
||||
public SyncController(ApplicationDbContext db)
|
||||
public SyncController(ApplicationDbContext db, IOptions<SyncOptions> syncOptions)
|
||||
{
|
||||
_db = db;
|
||||
_syncOptions = syncOptions.Value;
|
||||
_dynamo = new AmazonDynamoDBClient(Amazon.RegionEndpoint.USEast1);
|
||||
}
|
||||
|
||||
private IActionResult? SyncDisabledResult()
|
||||
{
|
||||
if (_syncOptions.Enabled)
|
||||
return null;
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable, new
|
||||
{
|
||||
message = "DynamoDB sync bridge is disabled. Use POST /api/workorders/ingest."
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("WorkOrders")]
|
||||
public async Task<IActionResult> SyncWorkOrders()
|
||||
{
|
||||
if (SyncDisabledResult() is { } disabled)
|
||||
return disabled;
|
||||
var synced = 0;
|
||||
var created = 0;
|
||||
var updated = 0;
|
||||
|
|
@ -62,12 +79,12 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
var existing = await _db.workOrders
|
||||
.FirstOrDefaultAsync(w => w.ExternalWorkOrderId == externalId);
|
||||
|
||||
var locationId = await ResolveLocationId(
|
||||
var (locationId, locationWasCreated) = await ResolveLocationId(
|
||||
GetString(item, "site_code"),
|
||||
GetString(item, "building"),
|
||||
GetString(item, "address"));
|
||||
|
||||
if (locationId.HasValue && !locationsCreated.Equals(0))
|
||||
if (locationWasCreated)
|
||||
locationsCreated++;
|
||||
|
||||
if (existing == null)
|
||||
|
|
@ -138,6 +155,9 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[HttpPost("Comments")]
|
||||
public async Task<IActionResult> SyncComments()
|
||||
{
|
||||
if (SyncDisabledResult() is { } disabled)
|
||||
return disabled;
|
||||
|
||||
var synced = 0;
|
||||
var created = 0;
|
||||
var skipped = 0;
|
||||
|
|
@ -212,6 +232,9 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[HttpPost("BackfillInternalWONumbers")]
|
||||
public async Task<IActionResult> BackfillInternalWONumbers()
|
||||
{
|
||||
if (SyncDisabledResult() is { } disabled)
|
||||
return disabled;
|
||||
|
||||
var wosMissing = await _db.workOrders
|
||||
.Where(w => w.InternalWONumber == null || w.InternalWONumber == "")
|
||||
.OrderBy(w => w.Id)
|
||||
|
|
@ -243,6 +266,9 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[HttpPost("BackfillDispatchNumbers")]
|
||||
public async Task<IActionResult> BackfillDispatchNumbers()
|
||||
{
|
||||
if (SyncDisabledResult() is { } disabled)
|
||||
return disabled;
|
||||
|
||||
var missing = await _db.Dispatches
|
||||
.Where(d => d.DispatchNumber == null || d.DispatchNumber == "")
|
||||
.OrderBy(d => d.Id)
|
||||
|
|
@ -270,6 +296,9 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[HttpPost("VendorReplies")]
|
||||
public async Task<IActionResult> SyncVendorReplies()
|
||||
{
|
||||
if (SyncDisabledResult() is { } disabled)
|
||||
return disabled;
|
||||
|
||||
var synced = 0;
|
||||
var created = 0;
|
||||
var skipped = 0;
|
||||
|
|
@ -341,6 +370,9 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[HttpPost("BackfillCommentTypes")]
|
||||
public async Task<IActionResult> BackfillCommentTypes()
|
||||
{
|
||||
if (SyncDisabledResult() is { } disabled)
|
||||
return disabled;
|
||||
|
||||
var updated = await _db.Database.ExecuteSqlRawAsync(
|
||||
"UPDATE Comments SET CommentType = 'customer' WHERE ExternalCommentId IS NOT NULL AND (CommentType IS NULL OR CommentType = '')");
|
||||
return Ok(new { updated });
|
||||
|
|
@ -349,6 +381,9 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[HttpPost("All")]
|
||||
public async Task<IActionResult> SyncAll()
|
||||
{
|
||||
if (SyncDisabledResult() is { } disabled)
|
||||
return disabled;
|
||||
|
||||
var woResult = await SyncWorkOrders() as OkObjectResult;
|
||||
var commentResult = await SyncComments() as OkObjectResult;
|
||||
|
||||
|
|
@ -359,17 +394,17 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
});
|
||||
}
|
||||
|
||||
private async Task<int?> ResolveLocationId(string? siteCode, string? building, string? address)
|
||||
private async Task<(int? LocationId, bool Created)> ResolveLocationId(string? siteCode, string? building, string? address)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(siteCode) && string.IsNullOrWhiteSpace(building))
|
||||
return null;
|
||||
return (null, false);
|
||||
|
||||
var matchCode = siteCode ?? building;
|
||||
var existing = await _db.Locations
|
||||
.FirstOrDefaultAsync(l => l.Name == matchCode || l.Title == matchCode);
|
||||
|
||||
if (existing != null)
|
||||
return existing.Id;
|
||||
return (existing.Id, false);
|
||||
|
||||
var location = new Locations
|
||||
{
|
||||
|
|
@ -382,7 +417,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
_db.Locations.Add(location);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return location.Id;
|
||||
return (location.Id, true);
|
||||
}
|
||||
|
||||
private static string? GetString(Dictionary<string, AttributeValue> item, string key)
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
v.Id,
|
||||
v.CompanyName,
|
||||
v.ContactName,
|
||||
v.TradeSpecialties,
|
||||
v.Address,
|
||||
v.City,
|
||||
|
|
@ -191,6 +192,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
v.Id,
|
||||
v.CompanyName,
|
||||
v.ContactName,
|
||||
v.TradeSpecialties,
|
||||
address = string.Join(", ", addr),
|
||||
distanceMiles = distance.HasValue ? Math.Round(distance.Value, 1) : (double?)null
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ using System.Security.Claims;
|
|||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Exceptions;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Controllers
|
||||
{
|
||||
// [Authorize]
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/WorkOrder")]
|
||||
[Route("api/workorders")]
|
||||
|
|
@ -42,6 +43,15 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
private readonly Helper.SendMessage _sendMessage;
|
||||
private readonly Helper.VendorPortalTokenService _vendorTokens;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IWorkOrderBoardService _workOrderBoardService;
|
||||
private readonly IWorkOrderBoardUpdateService _boardUpdateService;
|
||||
private readonly IWorkOrderBoardCreateService _boardCreateService;
|
||||
private readonly IWorkOrderBoardCancelService _boardCancelService;
|
||||
private readonly IWorkOrderAdvancedSearchService _advancedSearchService;
|
||||
private readonly IWorkOrderDetailService _workOrderDetailService;
|
||||
private readonly IWorkOrderCommentService _workOrderCommentService;
|
||||
private readonly IWorkOrderCompletionService _workOrderCompletionService;
|
||||
private readonly IWorkOrderMediaService _workOrderMediaService;
|
||||
|
||||
public WorkOrderController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
|
|
@ -58,7 +68,16 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
IHttpContextAccessor httpContext,
|
||||
Helper.SendMessage sendMessage,
|
||||
Helper.VendorPortalTokenService vendorTokens,
|
||||
IConfiguration config)
|
||||
IConfiguration config,
|
||||
IWorkOrderBoardService workOrderBoardService,
|
||||
IWorkOrderBoardUpdateService boardUpdateService,
|
||||
IWorkOrderBoardCreateService boardCreateService,
|
||||
IWorkOrderBoardCancelService boardCancelService,
|
||||
IWorkOrderAdvancedSearchService advancedSearchService,
|
||||
IWorkOrderDetailService workOrderDetailService,
|
||||
IWorkOrderCommentService workOrderCommentService,
|
||||
IWorkOrderCompletionService workOrderCompletionService,
|
||||
IWorkOrderMediaService workOrderMediaService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_workOrderService = workOrderService;
|
||||
|
|
@ -75,7 +94,390 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
_sendMessage = sendMessage;
|
||||
_vendorTokens = vendorTokens;
|
||||
_config = config;
|
||||
_workOrderBoardService = workOrderBoardService;
|
||||
_boardUpdateService = boardUpdateService;
|
||||
_boardCreateService = boardCreateService;
|
||||
_boardCancelService = boardCancelService;
|
||||
_advancedSearchService = advancedSearchService;
|
||||
_workOrderDetailService = workOrderDetailService;
|
||||
_workOrderCommentService = workOrderCommentService;
|
||||
_workOrderCompletionService = workOrderCompletionService;
|
||||
_workOrderMediaService = workOrderMediaService;
|
||||
}
|
||||
|
||||
[HttpGet("board")]
|
||||
public async Task<IActionResult> GetBoard(
|
||||
[FromQuery] DateOnly weekStart,
|
||||
[FromQuery] DateOnly? weekEnd = null,
|
||||
[FromQuery] List<string>? dispatchers = null,
|
||||
[FromQuery] bool myWorkOrders = false,
|
||||
[FromQuery] List<WorkOrderType>? types = null,
|
||||
[FromQuery] string? search = null)
|
||||
{
|
||||
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(new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("board/search")]
|
||||
public async Task<IActionResult> SearchBoard([FromQuery] WorkOrderAdvancedSearchQueryDto query)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var result = await _advancedSearchService.SearchAsync(query, userId);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("lookups/dispatchers")]
|
||||
public async Task<IActionResult> GetDispatcherLookups()
|
||||
{
|
||||
var result = await _workOrderBoardService.GetDispatcherLookupsAsync();
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpPatch("{id:int}/board")]
|
||||
public async Task<IActionResult> PatchBoardField(int id, [FromBody] WorkOrderBoardPatchRequestDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var row = await _boardUpdateService.PatchFieldAsync(id, request, actorId);
|
||||
return Ok(row);
|
||||
}
|
||||
catch (WorkOrderBoardConcurrencyException ex)
|
||||
{
|
||||
return Conflict(new WorkOrderBoardConflictDto
|
||||
{
|
||||
CurrentState = ex.CurrentState
|
||||
});
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto
|
||||
{
|
||||
Code = ex.Code,
|
||||
Message = ex.Message
|
||||
});
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("board")]
|
||||
public async Task<IActionResult> CreateBoardWorkOrder([FromBody] WorkOrderBoardCreateRequestDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var row = await _boardCreateService.CreateAsync(request, actorId);
|
||||
return Ok(row);
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "DuplicateWoNumber")
|
||||
{
|
||||
return Conflict(new WorkOrderBoardValidationErrorDto
|
||||
{
|
||||
Code = ex.Code,
|
||||
Message = ex.Message
|
||||
});
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto
|
||||
{
|
||||
Code = ex.Code,
|
||||
Message = ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/cancel")]
|
||||
public async Task<IActionResult> CancelBoardWorkOrder(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var row = await _boardCancelService.CancelAsync(id, actorId);
|
||||
return Ok(row);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto
|
||||
{
|
||||
Code = ex.Code,
|
||||
Message = ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/detail")]
|
||||
public async Task<IActionResult> GetDetail(int id)
|
||||
{
|
||||
var detail = await _workOrderDetailService.GetDetailAsync(id);
|
||||
if (detail == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Work order not found." });
|
||||
return Ok(detail);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/audit")]
|
||||
public async Task<IActionResult> GetAudit(int id, [FromQuery] int limit = 50)
|
||||
{
|
||||
var audit = await _workOrderDetailService.GetAuditAsync(id, limit);
|
||||
if (audit == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Work order not found." });
|
||||
return Ok(audit);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/comments")]
|
||||
public async Task<IActionResult> GetBoardComments(int id)
|
||||
{
|
||||
var comments = await _workOrderCommentService.GetCommentsAsync(id);
|
||||
if (comments == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Work order not found." });
|
||||
return Ok(comments);
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/comments")]
|
||||
public async Task<IActionResult> AddBoardComment(int id, [FromBody] WorkOrderCommentCreateDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var comment = await _workOrderCommentService.AddCommentAsync(id, request, actorId);
|
||||
return Ok(comment);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch("{id:int}/comments/{commentId:int}")]
|
||||
public async Task<IActionResult> UpdateBoardComment(
|
||||
int id,
|
||||
int commentId,
|
||||
[FromBody] WorkOrderCommentCreateDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var isAdmin = User.IsInRole("Admin");
|
||||
var comment = await _workOrderCommentService.UpdateCommentAsync(
|
||||
id, commentId, request, actorId, isAdmin);
|
||||
return Ok(comment);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden,
|
||||
new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("completion-templates")]
|
||||
public async Task<IActionResult> GetCompletionTemplates(
|
||||
[FromQuery] string? serviceKey = null,
|
||||
[FromQuery] WorkOrderType? workOrderType = null)
|
||||
{
|
||||
var templates = await _workOrderCompletionService.GetTemplatesAsync(serviceKey, workOrderType);
|
||||
return Ok(templates);
|
||||
}
|
||||
|
||||
[HttpGet("completion-templates/{id:int}")]
|
||||
public async Task<IActionResult> GetCompletionTemplate(int id)
|
||||
{
|
||||
var template = await _workOrderCompletionService.GetTemplateByIdAsync(id);
|
||||
if (template == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Template not found." });
|
||||
return Ok(template);
|
||||
}
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost("completion-templates")]
|
||||
public async Task<IActionResult> CreateCompletionTemplate([FromBody] CompletionDocTemplateCreateDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var created = await _workOrderCompletionService.CreateTemplateAsync(request);
|
||||
return Ok(created);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPut("completion-templates/{id:int}")]
|
||||
public async Task<IActionResult> UpdateCompletionTemplate(int id, [FromBody] CompletionDocTemplateCreateDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var updated = await _workOrderCompletionService.UpdateTemplateAsync(id, request);
|
||||
return Ok(updated);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("completion-templates/{id:int}")]
|
||||
public async Task<IActionResult> DeleteCompletionTemplate(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _workOrderCompletionService.DeleteTemplateAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/completion-doc")]
|
||||
[RequestSizeLimit(30_000_000)]
|
||||
public async Task<IActionResult> UploadCompletionDoc(
|
||||
int id,
|
||||
[FromForm] IFormFile file,
|
||||
[FromForm] string? signOffName = null,
|
||||
[FromForm] string? signOffSignature = null,
|
||||
[FromForm] string? workOrderVersion = null)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
return BadRequest(new Response { Status = "Error", Message = "file is required." });
|
||||
|
||||
try
|
||||
{
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var fileUrl = await SaveFileAsync(file);
|
||||
var result = await _workOrderCompletionService.UploadCompletionDocAsync(
|
||||
id,
|
||||
new WorkOrderCompletionDocUploadDto
|
||||
{
|
||||
SignOffName = signOffName,
|
||||
SignOffSignature = signOffSignature,
|
||||
WorkOrderVersion = workOrderVersion
|
||||
},
|
||||
fileUrl,
|
||||
actorId);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code is "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code is "ConcurrencyConflict")
|
||||
{
|
||||
return Conflict(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/media")]
|
||||
public async Task<IActionResult> GetMedia(int id)
|
||||
{
|
||||
var media = await _workOrderMediaService.GetMediaAsync(id);
|
||||
if (media == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Work order not found." });
|
||||
return Ok(media);
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/media")]
|
||||
[RequestSizeLimit(30_000_000)]
|
||||
public async Task<IActionResult> AddMedia(
|
||||
int id,
|
||||
[FromForm] WorkOrderMediaCategory category,
|
||||
[FromForm] IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
return BadRequest(new Response { Status = "Error", Message = "file is required." });
|
||||
|
||||
try
|
||||
{
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var fileUrl = await SaveFileAsync(file);
|
||||
var media = await _workOrderMediaService.AddMediaAsync(id, category, fileUrl, actorId);
|
||||
return Ok(media);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}/media/{mediaId:int}")]
|
||||
public async Task<IActionResult> DeleteMedia(int id, int mediaId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
await _workOrderMediaService.DeleteMediaAsync(id, mediaId, actorId);
|
||||
return NoContent();
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("AddWorkorder")]
|
||||
public async Task<IActionResult> Addworkorder([FromForm] Workorder_DTO model)
|
||||
|
|
@ -295,6 +697,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
}
|
||||
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete]
|
||||
[Route("DeleteWorkorder")]
|
||||
public async Task<IActionResult> Deleteworkorder([FromBody] EditWorkorder_DTO model)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
using Api.SeaHavenIndustries.Filters;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/workorders")]
|
||||
public class WorkOrderIngestController : ControllerBase
|
||||
{
|
||||
private const int MaxBatchSize = 100;
|
||||
private readonly IWorkOrderIngestService _ingestService;
|
||||
|
||||
public WorkOrderIngestController(IWorkOrderIngestService ingestService)
|
||||
{
|
||||
_ingestService = ingestService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Idempotent upsert by ExternalWorkOrderId (Lambda cutover target).
|
||||
/// Auth: X-Ingest-Key header.
|
||||
/// </summary>
|
||||
[HttpPost("ingest")]
|
||||
[IngestApiKey]
|
||||
[ProducesResponseType(typeof(WorkOrderIngestBatchResultDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<WorkOrderIngestBatchResultDto>> Ingest(
|
||||
[FromBody] WorkOrderIngestBatchRequestDto? batch,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var items = NormalizeItems(batch);
|
||||
if (items.Count == 0)
|
||||
return BadRequest("At least one item with externalWorkOrderId is required.");
|
||||
|
||||
if (items.Count > MaxBatchSize)
|
||||
return BadRequest($"Max {MaxBatchSize} items per request.");
|
||||
|
||||
var result = await _ingestService.UpsertBatchAsync(items, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
private static List<WorkOrderIngestPayloadDto> NormalizeItems(WorkOrderIngestBatchRequestDto? batch)
|
||||
{
|
||||
if (batch?.Items is { Count: > 0 })
|
||||
return batch.Items;
|
||||
|
||||
return new List<WorkOrderIngestPayloadDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
using Api.SeaHavenIndustries.Options;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Helpers;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Controllers
|
||||
{
|
||||
/// <summary>Ops-only endpoints for manual domain job reprocessing.</summary>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[ApiController]
|
||||
[Route("api/workorders/jobs")]
|
||||
public class WorkOrderJobsController : ControllerBase
|
||||
{
|
||||
private readonly IWorkOrderWeekRolledService _weekRolledService;
|
||||
private readonly IPastDueCacheService _pastDueCacheService;
|
||||
|
||||
public WorkOrderJobsController(
|
||||
IWorkOrderWeekRolledService weekRolledService,
|
||||
IPastDueCacheService pastDueCacheService)
|
||||
{
|
||||
_weekRolledService = weekRolledService;
|
||||
_pastDueCacheService = pastDueCacheService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reprocess WeekRolled for a specific operational week (Monday start).
|
||||
/// Idempotent via WorkOrderWeekRolledLedger.
|
||||
/// </summary>
|
||||
[HttpPost("week-rolled")]
|
||||
[ProducesResponseType(typeof(WeekRolledJobResult), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<WeekRolledJobResult>> RunWeekRolled(
|
||||
[FromQuery] DateOnly? sourceWeekStart,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var weekStart = sourceWeekStart
|
||||
?? WorkOrderOperationalWeek.GetPreviousOperationalWeekStart(DateTime.UtcNow);
|
||||
|
||||
if (weekStart.DayOfWeek != DayOfWeek.Monday)
|
||||
return BadRequest("sourceWeekStart must be a Monday (operational week start).");
|
||||
|
||||
var result = await _weekRolledService.ProcessWeekRolledAsync(weekStart, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh optional OperationalFlags.PastDue cache (board isPastDue remains on-read).
|
||||
/// </summary>
|
||||
[HttpPost("past-due-cache")]
|
||||
[ProducesResponseType(typeof(PastDueCacheJobResult), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PastDueCacheJobResult>> RunPastDueCache(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _pastDueCacheService.RefreshAsync(cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Api.SeaHavenIndustries/Controllers/WorkOrderOpsController.cs
Normal file
29
Api.SeaHavenIndustries/Controllers/WorkOrderOpsController.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Controllers
|
||||
{
|
||||
/// <summary>Ops health and coexistence metrics (Fase 7).</summary>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[ApiController]
|
||||
[Route("api/workorders/ops")]
|
||||
public class WorkOrderOpsController : ControllerBase
|
||||
{
|
||||
private readonly IWorkOrderOpsHealthService _healthService;
|
||||
|
||||
public WorkOrderOpsController(IWorkOrderOpsHealthService healthService)
|
||||
{
|
||||
_healthService = healthService;
|
||||
}
|
||||
|
||||
[HttpGet("health")]
|
||||
[ProducesResponseType(typeof(WorkOrderOpsHealthDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<WorkOrderOpsHealthDto>> GetHealth(CancellationToken cancellationToken)
|
||||
{
|
||||
var health = await _healthService.GetHealthAsync(cancellationToken);
|
||||
return Ok(health);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Api.SeaHavenIndustries/Filters/ConcurrencyExceptionFilter.cs
Normal file
22
Api.SeaHavenIndustries/Filters/ConcurrencyExceptionFilter.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Filters
|
||||
{
|
||||
public class ConcurrencyExceptionFilter : IExceptionFilter
|
||||
{
|
||||
public void OnException(ExceptionContext context)
|
||||
{
|
||||
if (context.Exception is not DbUpdateConcurrencyException) return;
|
||||
|
||||
context.Result = new ConflictObjectResult(new
|
||||
{
|
||||
status = "Conflict",
|
||||
message = "The record was modified by another user. Refresh and retry.",
|
||||
code = 409
|
||||
});
|
||||
context.ExceptionHandled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Api.SeaHavenIndustries/Filters/IngestApiKeyFilter.cs
Normal file
67
Api.SeaHavenIndustries/Filters/IngestApiKeyFilter.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Api.SeaHavenIndustries.Options;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Filters
|
||||
{
|
||||
/// <summary>Validates X-Ingest-Key for POST /api/workorders/ingest.</summary>
|
||||
public class IngestApiKeyFilter : IAsyncActionFilter
|
||||
{
|
||||
private readonly WorkOrderIngestOptions _options;
|
||||
|
||||
public IngestApiKeyFilter(IOptions<WorkOrderIngestOptions> options)
|
||||
{
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
if (!_options.Enabled)
|
||||
{
|
||||
context.Result = new ObjectResult(new { message = "Work order ingest is disabled." })
|
||||
{
|
||||
StatusCode = StatusCodes.Status503ServiceUnavailable
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_options.ApiKey)
|
||||
|| _options.ApiKey.Contains("${", StringComparison.Ordinal))
|
||||
{
|
||||
context.Result = new ObjectResult(new { message = "Ingest API key is not configured." })
|
||||
{
|
||||
StatusCode = StatusCodes.Status503ServiceUnavailable
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.HttpContext.Request.Headers.TryGetValue("X-Ingest-Key", out var provided)
|
||||
|| !FixedTimeEquals(provided.ToString(), _options.ApiKey))
|
||||
{
|
||||
context.Result = new UnauthorizedObjectResult(new { message = "Invalid or missing X-Ingest-Key." });
|
||||
return;
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
|
||||
private static bool FixedTimeEquals(string provided, string expected)
|
||||
{
|
||||
var providedBytes = Encoding.UTF8.GetBytes(provided);
|
||||
var expectedBytes = Encoding.UTF8.GetBytes(expected);
|
||||
return providedBytes.Length == expectedBytes.Length
|
||||
&& CryptographicOperations.FixedTimeEquals(providedBytes, expectedBytes);
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public sealed class IngestApiKeyAttribute : ServiceFilterAttribute
|
||||
{
|
||||
public IngestApiKeyAttribute() : base(typeof(IngestApiKeyFilter))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
using Microsoft.Extensions.Options;
|
||||
using SeaHaven.Services.Implementation;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using Api.SeaHavenIndustries.Options;
|
||||
|
||||
namespace Api.SeaHavenIndustries.HostedServices
|
||||
{
|
||||
public class PastDueCacheHostedService : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<PastDueCacheHostedService> _logger;
|
||||
private readonly PastDueCacheJobOptions _options;
|
||||
private readonly WorkOrderJobRunState _runState;
|
||||
private DateOnly? _lastRunDate;
|
||||
|
||||
public PastDueCacheHostedService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<WorkOrderJobsOptions> options,
|
||||
WorkOrderJobRunState runState,
|
||||
ILogger<PastDueCacheHostedService> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_options = options.Value.PastDueCache;
|
||||
_runState = runState;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (_options.Enabled && ShouldRunNow())
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
if (_lastRunDate != today)
|
||||
{
|
||||
if (await RunJobAsync(stoppingToken))
|
||||
_lastRunDate = today;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> RunJobAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var service = scope.ServiceProvider.GetRequiredService<IPastDueCacheService>();
|
||||
await service.RefreshAsync(stoppingToken);
|
||||
|
||||
_runState.LastPastDueCacheRunUtc = DateTime.UtcNow;
|
||||
_runState.LastPastDueCacheError = null;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_runState.LastPastDueCacheError = ex.Message;
|
||||
_logger.LogError(ex, "PastDue cache hosted job failed.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldRunNow()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (!TimeOnly.TryParse(_options.RunAtUtc, out var runAt))
|
||||
runAt = new TimeOnly(0, 10);
|
||||
|
||||
return TimeOnly.FromDateTime(now) >= runAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
using Microsoft.Extensions.Options;
|
||||
using SeaHaven.Services.Helpers;
|
||||
using SeaHaven.Services.Implementation;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using Api.SeaHavenIndustries.Options;
|
||||
|
||||
namespace Api.SeaHavenIndustries.HostedServices
|
||||
{
|
||||
public class WorkOrderWeekRolledHostedService : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<WorkOrderWeekRolledHostedService> _logger;
|
||||
private readonly WeekRolledJobOptions _options;
|
||||
private readonly WorkOrderJobRunState _runState;
|
||||
private DateOnly? _lastRunDate;
|
||||
|
||||
public WorkOrderWeekRolledHostedService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<WorkOrderJobsOptions> options,
|
||||
WorkOrderJobRunState runState,
|
||||
ILogger<WorkOrderWeekRolledHostedService> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_options = options.Value.WeekRolled;
|
||||
_runState = runState;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (_options.Enabled && ShouldRunNow())
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
if (_lastRunDate != today)
|
||||
{
|
||||
if (await RunJobAsync(stoppingToken))
|
||||
_lastRunDate = today;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> RunJobAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sourceWeekStart = WorkOrderOperationalWeek.GetPreviousOperationalWeekStart(DateTime.UtcNow);
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var service = scope.ServiceProvider.GetRequiredService<IWorkOrderWeekRolledService>();
|
||||
await service.ProcessWeekRolledAsync(sourceWeekStart, stoppingToken);
|
||||
|
||||
_runState.LastWeekRolledRunUtc = DateTime.UtcNow;
|
||||
_runState.LastWeekRolledError = null;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_runState.LastWeekRolledError = ex.Message;
|
||||
_logger.LogError(ex, "WeekRolled hosted job failed.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldRunNow()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (!Enum.TryParse<DayOfWeek>(_options.DayOfWeek, ignoreCase: true, out var targetDay))
|
||||
targetDay = DayOfWeek.Monday;
|
||||
|
||||
if (now.DayOfWeek != targetDay)
|
||||
return false;
|
||||
|
||||
if (!TimeOnly.TryParse(_options.RunAtUtc, out var runAt))
|
||||
runAt = new TimeOnly(0, 5);
|
||||
|
||||
return TimeOnly.FromDateTime(now) >= runAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
using Api.SeaHavenIndustries.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Middleware
|
||||
{
|
||||
/// <summary>Adds Sunset/Deprecation headers to legacy WorkOrder REST routes.</summary>
|
||||
public class LegacyDeprecationMiddleware
|
||||
{
|
||||
private static readonly HashSet<string> LegacyPathSegments = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"GetWorkOrderList",
|
||||
"AddWorkorder",
|
||||
"EditWorkorder",
|
||||
"ChangeStatus",
|
||||
"ChangeAssignment",
|
||||
"DeleteWorkorder",
|
||||
"GetWorkorderById"
|
||||
};
|
||||
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly LegacyEndpointOptions _options;
|
||||
|
||||
public LegacyDeprecationMiddleware(RequestDelegate next, IOptions<LegacyEndpointOptions> options)
|
||||
{
|
||||
_next = next;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (_options.DeprecationEnabled)
|
||||
{
|
||||
var path = context.Request.Path.Value ?? "";
|
||||
var isLegacyWo = (path.Contains("/WorkOrder", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.Contains("/workorders", StringComparison.OrdinalIgnoreCase))
|
||||
&& LegacyPathSegments.Any(seg => path.Contains(seg, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (isLegacyWo)
|
||||
{
|
||||
context.Response.OnStarting(() =>
|
||||
{
|
||||
context.Response.Headers["Deprecation"] = "true";
|
||||
if (!string.IsNullOrWhiteSpace(_options.SunsetDate)
|
||||
&& DateTimeOffset.TryParse(_options.SunsetDate, out var sunset))
|
||||
{
|
||||
context.Response.Headers["Sunset"] = sunset.UtcDateTime.ToString("r");
|
||||
}
|
||||
context.Response.Headers["Link"] = "</api/workorders/board>; rel=\"successor-version\"";
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Api.SeaHavenIndustries/Options/LegacyEndpointOptions.cs
Normal file
12
Api.SeaHavenIndustries/Options/LegacyEndpointOptions.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace Api.SeaHavenIndustries.Options
|
||||
{
|
||||
public class LegacyEndpointOptions
|
||||
{
|
||||
public const string SectionName = "LegacyEndpoints";
|
||||
|
||||
public bool DeprecationEnabled { get; set; }
|
||||
|
||||
/// <summary>ISO date returned in Sunset header (e.g. 2026-12-31).</summary>
|
||||
public string? SunsetDate { get; set; }
|
||||
}
|
||||
}
|
||||
10
Api.SeaHavenIndustries/Options/SyncOptions.cs
Normal file
10
Api.SeaHavenIndustries/Options/SyncOptions.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace Api.SeaHavenIndustries.Options
|
||||
{
|
||||
public class SyncOptions
|
||||
{
|
||||
public const string SectionName = "Sync";
|
||||
|
||||
/// <summary>When false, SyncController endpoints return 503 (post-Lambda cutover). Default false to avoid dual-run with ingest.</summary>
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
}
|
||||
13
Api.SeaHavenIndustries/Options/WorkOrderIngestOptions.cs
Normal file
13
Api.SeaHavenIndustries/Options/WorkOrderIngestOptions.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
namespace Api.SeaHavenIndustries.Options
|
||||
{
|
||||
public class WorkOrderIngestOptions
|
||||
{
|
||||
public const string SectionName = "WorkOrderIngest";
|
||||
|
||||
/// <summary>When false, ingest endpoints return 503. Default false until a real ApiKey is provisioned.</summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>Shared secret for X-Ingest-Key header (Lambda / service accounts).</summary>
|
||||
public string? ApiKey { get; set; }
|
||||
}
|
||||
}
|
||||
23
Api.SeaHavenIndustries/Options/WorkOrderJobsOptions.cs
Normal file
23
Api.SeaHavenIndustries/Options/WorkOrderJobsOptions.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
namespace Api.SeaHavenIndustries.Options
|
||||
{
|
||||
public class WorkOrderJobsOptions
|
||||
{
|
||||
public const string SectionName = "WorkOrderJobs";
|
||||
|
||||
public WeekRolledJobOptions WeekRolled { get; set; } = new();
|
||||
public PastDueCacheJobOptions PastDueCache { get; set; } = new();
|
||||
}
|
||||
|
||||
public class WeekRolledJobOptions
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public string RunAtUtc { get; set; } = "00:05";
|
||||
public string DayOfWeek { get; set; } = "Monday";
|
||||
}
|
||||
|
||||
public class PastDueCacheJobOptions
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
public string RunAtUtc { get; set; } = "00:10";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
using Api.SeaHavenIndustries.Helper;
|
||||
using Api.SeaHavenIndustries.HostedServices;
|
||||
using Api.SeaHavenIndustries.Middleware;
|
||||
using Api.SeaHavenIndustries.Options;
|
||||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
|
@ -9,6 +12,7 @@ using Microsoft.OpenApi.Models;
|
|||
using System.Text;
|
||||
using SeaHaven.DataServices.DependencyInjection;
|
||||
using SeaHaven.Services.DependencyInjection;
|
||||
using SeaHaven.Services.Implementation;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
ConfigurationManager configuration = builder.Configuration;
|
||||
|
|
@ -22,7 +26,10 @@ builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
|
|||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
builder.Services.AddControllers(options =>
|
||||
{
|
||||
options.Filters.Add<Api.SeaHavenIndustries.Filters.ConcurrencyExceptionFilter>();
|
||||
}).AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.MaxDepth = 264; // or any other desired value
|
||||
});
|
||||
|
|
@ -48,6 +55,38 @@ builder.Services.AddSingleton<Api.SeaHavenIndustries.Helper.ZipCodeDistance>(sp
|
|||
builder.Services.AddDataServices();
|
||||
builder.Services.AddBusinessServices();
|
||||
|
||||
builder.Services.Configure<WorkOrderJobsOptions>(
|
||||
builder.Configuration.GetSection(WorkOrderJobsOptions.SectionName));
|
||||
builder.Services.AddOptions<WorkOrderIngestOptions>()
|
||||
.Bind(builder.Configuration.GetSection(WorkOrderIngestOptions.SectionName))
|
||||
.Validate(
|
||||
o => !o.Enabled
|
||||
|| (!string.IsNullOrWhiteSpace(o.ApiKey)
|
||||
&& !o.ApiKey.Contains("${", StringComparison.Ordinal)
|
||||
&& o.ApiKey.Length >= 32),
|
||||
"WorkOrderIngest:ApiKey must be a real secret (env/user-secrets, length >= 32) when ingest is enabled.")
|
||||
.ValidateOnStart();
|
||||
builder.Services.Configure<SyncOptions>(
|
||||
builder.Configuration.GetSection(SyncOptions.SectionName));
|
||||
builder.Services.Configure<LegacyEndpointOptions>(
|
||||
builder.Configuration.GetSection(LegacyEndpointOptions.SectionName));
|
||||
builder.Services.AddSingleton<WorkOrderJobRunStateAccessor>();
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<WorkOrderJobRunStateAccessor>().State);
|
||||
builder.Services.AddScoped<Api.SeaHavenIndustries.Filters.IngestApiKeyFilter>();
|
||||
builder.Services.AddHostedService<WorkOrderWeekRolledHostedService>();
|
||||
builder.Services.AddHostedService<PastDueCacheHostedService>();
|
||||
builder.Services.AddOptions<SeaHaven.Services.Implementation.WorkOrderOpsHealthOptions>()
|
||||
.Configure<Microsoft.Extensions.Options.IOptions<SyncOptions>,
|
||||
Microsoft.Extensions.Options.IOptions<WorkOrderIngestOptions>,
|
||||
Microsoft.Extensions.Options.IOptions<LegacyEndpointOptions>>(
|
||||
(health, sync, ingest, legacy) =>
|
||||
{
|
||||
health.SyncEnabled = sync.Value.Enabled;
|
||||
health.IngestEnabled = ingest.Value.Enabled;
|
||||
health.LegacyDeprecationEnabled = legacy.Value.DeprecationEnabled;
|
||||
health.LegacySunsetDate = legacy.Value.SunsetDate;
|
||||
});
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
|
|
@ -113,6 +152,7 @@ if (!app.Environment.IsDevelopment())
|
|||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseCors();
|
||||
app.UseMiddleware<LegacyDeprecationMiddleware>();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
//"DefaultConnection": "Server=DESKTOP-64LC14O\SQLEXPRESS;Database=SeahavenIndustries;TrustServerCertificate=True;Trusted_Connection=True;MultipleActiveResultSets=true",
|
||||
|
||||
|
|
@ -23,5 +23,22 @@
|
|||
"ValidIssuer": "http://localhost:7195",
|
||||
// Provide the real value via environment variable JWT__Secret or user-secrets.
|
||||
"Secret": "${JWT_SECRET}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WorkOrderJobs": {
|
||||
"WeekRolled": { "Enabled": true, "RunAtUtc": "00:05", "DayOfWeek": "Monday" },
|
||||
"PastDueCache": { "Enabled": false, "RunAtUtc": "00:10" }
|
||||
},
|
||||
"WorkOrderIngest": {
|
||||
"Enabled": false,
|
||||
"ApiKey": "${WORKORDER_INGEST_API_KEY}"
|
||||
},
|
||||
"Sync": {
|
||||
"Enabled": true
|
||||
},
|
||||
"LegacyEndpoints": {
|
||||
"DeprecationEnabled": false,
|
||||
"SunsetDate": "2026-12-31"
|
||||
},
|
||||
"FrontendBaseUrl": "https://shoc.seahaven.com"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,77 @@ namespace Data.SeaHavenIndustries
|
|||
.HasIndex(u => new { u.DispatchId, u.Status })
|
||||
.IsUnique()
|
||||
.HasFilter("[Status] = 'Pending'");
|
||||
|
||||
builder.Entity<WorkOrderFieldLock>()
|
||||
.HasIndex(l => new { l.WorkOrderId, l.FieldName })
|
||||
.IsUnique();
|
||||
|
||||
builder.Entity<WorkOrderWeekRolledLedger>()
|
||||
.HasIndex(l => new { l.WorkOrderId, l.SourceWeekStart })
|
||||
.IsUnique();
|
||||
|
||||
builder.Entity<CompletionDocTemplate>()
|
||||
.HasIndex(t => new { t.ServiceKey, t.IsActive });
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.Property(w => w.RowVersion)
|
||||
.IsRowVersion();
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.Property(w => w.SiteCode)
|
||||
.HasMaxLength(450);
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.HasIndex(w => w.SiteCode)
|
||||
.HasFilter("[istemplate] = 0");
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.Property(w => w.InternalWONumber)
|
||||
.HasMaxLength(450);
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.HasIndex(w => w.InternalWONumber)
|
||||
.HasFilter("[istemplate] = 0");
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.HasIndex(w => new { w.LifecycleStatus, w.ScheduledDate })
|
||||
.HasFilter("[istemplate] = 0");
|
||||
|
||||
// Phase 1 board indexes (must stay in model to match snapshot / existing DBs).
|
||||
builder.Entity<WorkOrder>()
|
||||
.HasIndex(w => w.AssignTo);
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.HasIndex(w => w.ScheduledDate);
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.HasIndex(w => new { w.AssignTo, w.ScheduledDate });
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.HasIndex(w => w.WorkOrderType);
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.Property(w => w.Service)
|
||||
.HasMaxLength(128);
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.Property(w => w.ExternalWorkOrderId)
|
||||
.HasMaxLength(450);
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.HasIndex(w => w.ExternalWorkOrderId)
|
||||
.IsUnique()
|
||||
.HasFilter("[ExternalWorkOrderId] IS NOT NULL AND [ExternalWorkOrderId] <> ''");
|
||||
|
||||
builder.Entity<Dispatch>()
|
||||
.Property(d => d.RowVersion)
|
||||
.IsRowVersion();
|
||||
|
||||
builder.Entity<WorkOrder>()
|
||||
.HasOne(w => w.PrimaryDispatch)
|
||||
.WithMany()
|
||||
.HasForeignKey(w => w.PrimaryDispatchId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
public DbSet<Category> Categories { get; set; }
|
||||
public DbSet<Locations> Locations { get; set; }
|
||||
|
|
@ -46,7 +117,10 @@ namespace Data.SeaHavenIndustries
|
|||
public DbSet<WorkOrderContacts> WorkOrderContacts { get; set; }
|
||||
public DbSet<WorkOrderCategories> workOrderCategories { get; set; }
|
||||
public DbSet<WorkOrderAttachments> workOrderAttachments { get; set; }
|
||||
public DbSet<CompletionDocTemplate> CompletionDocTemplates { get; set; }
|
||||
public DbSet<WorkOrderAuditLog> WorkOrderAuditLogs { get; set; }
|
||||
public DbSet<WorkOrderFieldLock> WorkOrderFieldLocks { get; set; }
|
||||
public DbSet<WorkOrderWeekRolledLedger> WorkOrderWeekRolledLedgers { get; set; }
|
||||
public DbSet<DropdownOption> DropdownOptions { get; set; }
|
||||
public DbSet<Vendor> Vendors { get; set; }
|
||||
public DbSet<VendorAccessToken> VendorAccessTokens { get; set; }
|
||||
|
|
@ -75,6 +149,51 @@ namespace Data.SeaHavenIndustries
|
|||
public DbSet<Department> Departments { get; set; }
|
||||
public DbSet<JobTitle> JobTitles { get; set; }
|
||||
public DbSet<Region> Regions { get; set; }
|
||||
|
||||
public override int SaveChanges()
|
||||
{
|
||||
BumpRowVersionsForInMemoryProvider();
|
||||
return base.SaveChanges();
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
BumpRowVersionsForInMemoryProvider();
|
||||
return base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private void BumpRowVersionsForInMemoryProvider()
|
||||
{
|
||||
if (!Database.ProviderName?.Contains("InMemory", StringComparison.OrdinalIgnoreCase) ?? true)
|
||||
return;
|
||||
|
||||
foreach (var entry in ChangeTracker.Entries<WorkOrder>())
|
||||
{
|
||||
if (entry.State == EntityState.Modified)
|
||||
entry.Entity.RowVersion = IncrementRowVersion(entry.Entity.RowVersion);
|
||||
}
|
||||
|
||||
foreach (var entry in ChangeTracker.Entries<Dispatch>())
|
||||
{
|
||||
if (entry.State == EntityState.Modified)
|
||||
entry.Entity.RowVersion = IncrementRowVersion(entry.Entity.RowVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] IncrementRowVersion(byte[]? current)
|
||||
{
|
||||
var bytes = current != null && current.Length > 0
|
||||
? (byte[])current.Clone()
|
||||
: new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 };
|
||||
|
||||
for (var i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
if (++bytes[i] != 0)
|
||||
break;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
}
|
||||
public class ApplicationUser : IdentityUser
|
||||
|
|
@ -88,6 +207,8 @@ namespace Data.SeaHavenIndustries
|
|||
public DateTime? DeletedDate { get; set; }
|
||||
public bool? IsDeleted { get; set; }
|
||||
public string? UserImage { get; set; }
|
||||
public string? Initials { get; set; }
|
||||
public string? Color { get; set; }
|
||||
public int? Type { get; set; } // 1 for users 0 for admin
|
||||
public ICollection<Template>? Templates { get; set; }
|
||||
public ICollection<WorkOrder>? WorkOrders { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,92 +1,171 @@
|
|||
namespace Data.SeaHavenIndustries.Enums
|
||||
{
|
||||
public enum WorkOrderStatus
|
||||
{
|
||||
Open = 1,
|
||||
InProgress = 2,
|
||||
Completed = 3,
|
||||
Cancelled = 4,
|
||||
OnHold = 5
|
||||
}
|
||||
|
||||
public enum WorkOrderPriority
|
||||
{
|
||||
Low = 1,
|
||||
Medium = 2,
|
||||
High = 3,
|
||||
Critical = 4
|
||||
}
|
||||
|
||||
public enum AuditAction
|
||||
{
|
||||
Create = 1,
|
||||
Edit = 2,
|
||||
Delete = 3,
|
||||
StatusChange = 4,
|
||||
AssignmentChange = 5,
|
||||
Dispatch = 6,
|
||||
DispatchUpdate = 7,
|
||||
DispatchVerify = 8
|
||||
}
|
||||
|
||||
public enum ContactType
|
||||
{
|
||||
Primary = 1,
|
||||
Secondary = 2,
|
||||
Billing = 3,
|
||||
Technical = 4
|
||||
}
|
||||
|
||||
public enum AssetStatus
|
||||
{
|
||||
Active = 1,
|
||||
Inactive = 2,
|
||||
UnderMaintenance = 3,
|
||||
Retired = 4
|
||||
}
|
||||
|
||||
public enum DispatchStatus
|
||||
{
|
||||
Pending = 1,
|
||||
Assigned = 2,
|
||||
InProgress = 3,
|
||||
Completed = 4,
|
||||
Cancelled = 5
|
||||
}
|
||||
|
||||
public enum FollowUpStatus
|
||||
{
|
||||
Open = 1,
|
||||
InProgress = 2,
|
||||
Resolved = 3,
|
||||
Closed = 4
|
||||
}
|
||||
|
||||
public enum PMScheduleFrequency
|
||||
{
|
||||
Daily = 1,
|
||||
Weekly = 2,
|
||||
Monthly = 3,
|
||||
Quarterly = 4,
|
||||
SemiAnnual = 5,
|
||||
Annual = 6
|
||||
}
|
||||
|
||||
public enum CommentType
|
||||
{
|
||||
General = 1,
|
||||
Internal = 2,
|
||||
Customer = 3,
|
||||
Vendor = 4,
|
||||
System = 5
|
||||
}
|
||||
|
||||
public enum RecordType
|
||||
{
|
||||
WorkOrder = 1,
|
||||
Dispatch = 2,
|
||||
Quote = 3,
|
||||
Contact = 4,
|
||||
Asset = 5
|
||||
}
|
||||
}
|
||||
namespace Data.SeaHavenIndustries.Enums
|
||||
{
|
||||
/// <summary>Legacy enum — prefer LifecycleStatus for board operations.</summary>
|
||||
public enum WorkOrderStatus
|
||||
{
|
||||
Open = 1,
|
||||
InProgress = 2,
|
||||
Completed = 3,
|
||||
Cancelled = 4,
|
||||
OnHold = 5
|
||||
}
|
||||
|
||||
/// <summary>10 operational lifecycle values aligned with SHOC board (FE labels).</summary>
|
||||
public enum LifecycleStatus
|
||||
{
|
||||
Incomplete = 1,
|
||||
Pending = 2,
|
||||
Scheduled = 3,
|
||||
EnRoute = 4,
|
||||
OnSite = 5,
|
||||
InProgress = 6,
|
||||
Completed = 7,
|
||||
Rescheduled = 8,
|
||||
Canceled = 9,
|
||||
PendingQuote = 10
|
||||
}
|
||||
|
||||
/// <summary>Board work-order types. Legacy values retained for backward compatibility.</summary>
|
||||
public enum WorkOrderType
|
||||
{
|
||||
PO = 1,
|
||||
PM = 2,
|
||||
Emergency = 3,
|
||||
Project = 4,
|
||||
Inspection = 5,
|
||||
Reactive = 6,
|
||||
AddOn = 7,
|
||||
Other = 99
|
||||
}
|
||||
|
||||
public enum DocStatus
|
||||
{
|
||||
Yes = 1,
|
||||
No = 2,
|
||||
NotNeeded = 3
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum OperationalFlags
|
||||
{
|
||||
None = 0,
|
||||
PastDue = 1
|
||||
}
|
||||
|
||||
public enum AuditEventType
|
||||
{
|
||||
Manual = 1,
|
||||
System = 2,
|
||||
Sync = 3,
|
||||
Vendor = 4
|
||||
}
|
||||
|
||||
public enum AuditActorType
|
||||
{
|
||||
Dispatcher = 1,
|
||||
Vendor = 2,
|
||||
System = 3,
|
||||
Sync = 4
|
||||
}
|
||||
|
||||
public enum AuditActionType
|
||||
{
|
||||
FieldChanged = 1,
|
||||
StatusChanged = 2,
|
||||
AssignmentChanged = 3,
|
||||
WeekRolled = 4,
|
||||
SyncRejected = 5,
|
||||
Create = 6,
|
||||
Delete = 7
|
||||
}
|
||||
|
||||
public enum WorkOrderPriority
|
||||
{
|
||||
Low = 1,
|
||||
Medium = 2,
|
||||
High = 3,
|
||||
Critical = 4
|
||||
}
|
||||
|
||||
public enum AuditAction
|
||||
{
|
||||
Create = 1,
|
||||
Edit = 2,
|
||||
Delete = 3,
|
||||
StatusChange = 4,
|
||||
AssignmentChange = 5,
|
||||
Dispatch = 6,
|
||||
DispatchUpdate = 7,
|
||||
DispatchVerify = 8
|
||||
}
|
||||
|
||||
public enum ContactType
|
||||
{
|
||||
Primary = 1,
|
||||
Secondary = 2,
|
||||
Billing = 3,
|
||||
Technical = 4
|
||||
}
|
||||
|
||||
public enum AssetStatus
|
||||
{
|
||||
Active = 1,
|
||||
Inactive = 2,
|
||||
UnderMaintenance = 3,
|
||||
Retired = 4
|
||||
}
|
||||
|
||||
public enum DispatchStatus
|
||||
{
|
||||
Pending = 1,
|
||||
Assigned = 2,
|
||||
InProgress = 3,
|
||||
Completed = 4,
|
||||
Cancelled = 5
|
||||
}
|
||||
|
||||
public enum FollowUpStatus
|
||||
{
|
||||
Open = 1,
|
||||
InProgress = 2,
|
||||
Resolved = 3,
|
||||
Closed = 4
|
||||
}
|
||||
|
||||
public enum PMScheduleFrequency
|
||||
{
|
||||
Daily = 1,
|
||||
Weekly = 2,
|
||||
Monthly = 3,
|
||||
Quarterly = 4,
|
||||
SemiAnnual = 5,
|
||||
Annual = 6
|
||||
}
|
||||
|
||||
public enum CommentType
|
||||
{
|
||||
General = 1,
|
||||
Internal = 2,
|
||||
Customer = 3,
|
||||
Vendor = 4,
|
||||
System = 5
|
||||
}
|
||||
|
||||
public enum RecordType
|
||||
{
|
||||
WorkOrder = 1,
|
||||
Dispatch = 2,
|
||||
Quote = 3,
|
||||
Contact = 4,
|
||||
Asset = 5
|
||||
}
|
||||
|
||||
public enum WorkOrderMediaCategory
|
||||
{
|
||||
Before = 1,
|
||||
After = 2,
|
||||
Extra = 3,
|
||||
Completion = 4
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
public class AddWorkOrderTypeAndScheduledDateIndex : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
IF COL_LENGTH('workOrders', 'WorkOrderType') IS NULL
|
||||
ALTER TABLE workOrders ADD WorkOrderType varchar(50) NULL;
|
||||
""");
|
||||
|
||||
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
|
||||
""");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
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;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
IF COL_LENGTH('workOrders', 'WorkOrderType') IS NOT NULL
|
||||
ALTER TABLE workOrders DROP COLUMN WorkOrderType;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
2991
Data.SeaHavenIndustries/Migrations/20260624163145_Phase0_DomainFoundation.Designer.cs
generated
Normal file
2991
Data.SeaHavenIndustries/Migrations/20260624163145_Phase0_DomainFoundation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,744 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Phase0_DomainFoundation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Dispatches_workOrders_WorkOrderId",
|
||||
table: "Dispatches");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Locations_Accounts_AccountId",
|
||||
table: "Locations");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Locations_AccountId",
|
||||
table: "Locations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedDate",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeleterUserId",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletionTime",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModificationTime",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModifierUserId",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "createdby",
|
||||
table: "Regions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AccountId",
|
||||
table: "Locations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedDate",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeleterUserId",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletionTime",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModificationTime",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModifierUserId",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "createdby",
|
||||
table: "JobTitles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedDate",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeleterUserId",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletionTime",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModificationTime",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModifierUserId",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "createdby",
|
||||
table: "EmployeeEmails");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedDate",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeleterUserId",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletionTime",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModificationTime",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastModifierUserId",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "createdby",
|
||||
table: "Departments");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "ZipCode",
|
||||
table: "Locations",
|
||||
newName: "Zip");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Phone",
|
||||
table: "Locations",
|
||||
newName: "PhoneNumber");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "ContactEmail",
|
||||
table: "Locations",
|
||||
newName: "Email");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Contact",
|
||||
table: "Locations",
|
||||
newName: "Address2");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Address",
|
||||
table: "Locations",
|
||||
newName: "Address1");
|
||||
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
name: "AssignDate",
|
||||
table: "workOrders",
|
||||
type: "date",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AvettaTask",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "CarriedOver",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DocStatus",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "LegacyStatus",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "LifecycleStatus",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "OperationalFlags",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
name: "OriginalDate",
|
||||
table: "workOrders",
|
||||
type: "date",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
name: "OriginalWeek",
|
||||
table: "workOrders",
|
||||
type: "date",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "PrimaryDispatchId",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "RescheduleCount",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "workOrders",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ScheduleWeekOnly",
|
||||
table: "workOrders",
|
||||
type: "bit",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ScheduledEnd",
|
||||
table: "workOrders",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
name: "TargetWeek",
|
||||
table: "workOrders",
|
||||
type: "date",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "WorkOrderType",
|
||||
table: "workOrders",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Service",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(128)",
|
||||
maxLength: 128,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Notes",
|
||||
table: "WorkOrderContacts",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ActorType",
|
||||
table: "WorkOrderAuditLogs",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CorrelationId",
|
||||
table: "WorkOrderAuditLogs",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DispatchId",
|
||||
table: "WorkOrderAuditLogs",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "EventType",
|
||||
table: "WorkOrderAuditLogs",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "Dispatches",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Color",
|
||||
table: "AspNetUsers",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Initials",
|
||||
table: "AspNetUsers",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WorkOrderFieldLocks",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
WorkOrderId = table.Column<int>(type: "int", nullable: false),
|
||||
FieldName = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
LockedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
LockedByUserId = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WorkOrderFieldLocks", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_WorkOrderFieldLocks_workOrders_WorkOrderId",
|
||||
column: x => x.WorkOrderId,
|
||||
principalTable: "workOrders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_workOrders_PrimaryDispatchId",
|
||||
table: "workOrders",
|
||||
column: "PrimaryDispatchId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WorkOrderFieldLocks_WorkOrderId_FieldName",
|
||||
table: "WorkOrderFieldLocks",
|
||||
columns: new[] { "WorkOrderId", "FieldName" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Dispatches_workOrders_WorkOrderId",
|
||||
table: "Dispatches",
|
||||
column: "WorkOrderId",
|
||||
principalTable: "workOrders",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_workOrders_Dispatches_PrimaryDispatchId",
|
||||
table: "workOrders",
|
||||
column: "PrimaryDispatchId",
|
||||
principalTable: "Dispatches",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE workOrders SET LegacyStatus = Status WHERE LegacyStatus IS NULL AND Status IS NOT NULL;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE workOrders SET LifecycleStatus = CASE
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('open', 'incomplete') THEN 1
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('pending', 'onhold', 'on hold') THEN 2
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('scheduled') THEN 3
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('en route', 'enroute') THEN 4
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('on site', 'onsite') THEN 5
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('inprogress', 'in progress') THEN 6
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('completed', 'complete', 'done', 'closed') THEN 7
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('rescheduled') THEN 8
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('cancelled', 'canceled') THEN 9
|
||||
WHEN LOWER(LTRIM(RTRIM(Status))) IN ('pending quote', 'pendingquote', 'pendingapproval', 'pending approval') THEN 10
|
||||
ELSE 1
|
||||
END
|
||||
WHERE LifecycleStatus IS NULL AND Status IS NOT NULL;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE wo SET wo.PrimaryDispatchId = latest.DispatchId
|
||||
FROM workOrders wo
|
||||
INNER JOIN (
|
||||
SELECT WorkOrderId, MAX(Id) AS DispatchId
|
||||
FROM Dispatches
|
||||
WHERE WorkOrderId IS NOT NULL
|
||||
GROUP BY WorkOrderId
|
||||
) latest ON wo.Id = latest.WorkOrderId
|
||||
WHERE wo.PrimaryDispatchId IS NULL;
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Dispatches_workOrders_WorkOrderId",
|
||||
table: "Dispatches");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_workOrders_Dispatches_PrimaryDispatchId",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "WorkOrderFieldLocks");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_workOrders_PrimaryDispatchId",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AssignDate",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AvettaTask",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CarriedOver",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DocStatus",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LegacyStatus",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LifecycleStatus",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OperationalFlags",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OriginalDate",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OriginalWeek",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PrimaryDispatchId",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RescheduleCount",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ScheduleWeekOnly",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ScheduledEnd",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TargetWeek",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "WorkOrderType",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Service",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Notes",
|
||||
table: "WorkOrderContacts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ActorType",
|
||||
table: "WorkOrderAuditLogs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CorrelationId",
|
||||
table: "WorkOrderAuditLogs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DispatchId",
|
||||
table: "WorkOrderAuditLogs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EventType",
|
||||
table: "WorkOrderAuditLogs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "Dispatches");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Color",
|
||||
table: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Initials",
|
||||
table: "AspNetUsers");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Zip",
|
||||
table: "Locations",
|
||||
newName: "ZipCode");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "PhoneNumber",
|
||||
table: "Locations",
|
||||
newName: "Phone");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Email",
|
||||
table: "Locations",
|
||||
newName: "ContactEmail");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Address2",
|
||||
table: "Locations",
|
||||
newName: "Contact");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Address1",
|
||||
table: "Locations",
|
||||
newName: "Address");
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedDate",
|
||||
table: "Regions",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DeleterUserId",
|
||||
table: "Regions",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "DeletionTime",
|
||||
table: "Regions",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
table: "Regions",
|
||||
type: "bit",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastModificationTime",
|
||||
table: "Regions",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "LastModifierUserId",
|
||||
table: "Regions",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "createdby",
|
||||
table: "Regions",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AccountId",
|
||||
table: "Locations",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedDate",
|
||||
table: "JobTitles",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DeleterUserId",
|
||||
table: "JobTitles",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "DeletionTime",
|
||||
table: "JobTitles",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
table: "JobTitles",
|
||||
type: "bit",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastModificationTime",
|
||||
table: "JobTitles",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "LastModifierUserId",
|
||||
table: "JobTitles",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "createdby",
|
||||
table: "JobTitles",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedDate",
|
||||
table: "EmployeeEmails",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DeleterUserId",
|
||||
table: "EmployeeEmails",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "DeletionTime",
|
||||
table: "EmployeeEmails",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
table: "EmployeeEmails",
|
||||
type: "bit",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastModificationTime",
|
||||
table: "EmployeeEmails",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "LastModifierUserId",
|
||||
table: "EmployeeEmails",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "createdby",
|
||||
table: "EmployeeEmails",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedDate",
|
||||
table: "Departments",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DeleterUserId",
|
||||
table: "Departments",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "DeletionTime",
|
||||
table: "Departments",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
table: "Departments",
|
||||
type: "bit",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastModificationTime",
|
||||
table: "Departments",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "LastModifierUserId",
|
||||
table: "Departments",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "createdby",
|
||||
table: "Departments",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Locations_AccountId",
|
||||
table: "Locations",
|
||||
column: "AccountId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Dispatches_workOrders_WorkOrderId",
|
||||
table: "Dispatches",
|
||||
column: "WorkOrderId",
|
||||
principalTable: "workOrders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Locations_Accounts_AccountId",
|
||||
table: "Locations",
|
||||
column: "AccountId",
|
||||
principalTable: "Accounts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
3006
Data.SeaHavenIndustries/Migrations/20260624200000_Phase4_SearchIndexes.Designer.cs
generated
Normal file
3006
Data.SeaHavenIndustries/Migrations/20260624200000_Phase4_SearchIndexes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,89 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Phase4_SearchIndexes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// nvarchar(max) cannot be an index key; shrink before indexing.
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "SiteCode",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(450)",
|
||||
maxLength: 450,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "InternalWONumber",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(450)",
|
||||
maxLength: 450,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_workOrders_SiteCode",
|
||||
table: "workOrders",
|
||||
column: "SiteCode",
|
||||
filter: "[istemplate] = 0");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_workOrders_InternalWONumber",
|
||||
table: "workOrders",
|
||||
column: "InternalWONumber",
|
||||
filter: "[istemplate] = 0");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_workOrders_LifecycleStatus_ScheduledDate",
|
||||
table: "workOrders",
|
||||
columns: new[] { "LifecycleStatus", "ScheduledDate" },
|
||||
filter: "[istemplate] = 0");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_workOrders_SiteCode",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_workOrders_InternalWONumber",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_workOrders_LifecycleStatus_ScheduledDate",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "SiteCode",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(450)",
|
||||
oldMaxLength: 450,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "InternalWONumber",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(450)",
|
||||
oldMaxLength: 450,
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260625120000_Phase6_CompletionSlideOver")]
|
||||
public partial class Phase6_CompletionSlideOver : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"
|
||||
IF COL_LENGTH('workOrderAttachments', 'Category') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE workOrderAttachments ADD Category int NULL;
|
||||
END
|
||||
");
|
||||
|
||||
migrationBuilder.Sql(@"
|
||||
IF OBJECT_ID(N'dbo.CompletionDocTemplates', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE CompletionDocTemplates (
|
||||
Id int NOT NULL IDENTITY(1,1),
|
||||
Name nvarchar(max) NOT NULL,
|
||||
ServiceKey nvarchar(450) NOT NULL,
|
||||
WorkOrderType int NULL,
|
||||
TemplateUrl nvarchar(max) NOT NULL,
|
||||
IsActive bit NOT NULL,
|
||||
IsDeleted bit NULL,
|
||||
createdby nvarchar(max) NULL,
|
||||
DeleterUserId nvarchar(max) NULL,
|
||||
DeletionTime datetime2 NULL,
|
||||
CreatedDate datetime2 NULL,
|
||||
LastModificationTime datetime2 NULL,
|
||||
LastModifierUserId int NULL,
|
||||
CONSTRAINT PK_CompletionDocTemplates PRIMARY KEY (Id)
|
||||
);
|
||||
END
|
||||
");
|
||||
|
||||
migrationBuilder.Sql(@"
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM sys.indexes
|
||||
WHERE name = N'IX_CompletionDocTemplates_ServiceKey_IsActive'
|
||||
AND object_id = OBJECT_ID(N'dbo.CompletionDocTemplates'))
|
||||
BEGIN
|
||||
CREATE INDEX IX_CompletionDocTemplates_ServiceKey_IsActive
|
||||
ON CompletionDocTemplates (ServiceKey, IsActive);
|
||||
END
|
||||
");
|
||||
|
||||
migrationBuilder.Sql(@"
|
||||
UPDATE workOrders
|
||||
SET DocStatus = 1
|
||||
WHERE DocStatus IS NULL
|
||||
AND SignOffAttachment IS NOT NULL
|
||||
AND LTRIM(RTRIM(SignOffAttachment)) <> '';
|
||||
");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"
|
||||
UPDATE workOrders
|
||||
SET DocStatus = NULL
|
||||
WHERE DocStatus = 1
|
||||
AND SignOffAttachment IS NOT NULL
|
||||
AND LTRIM(RTRIM(SignOffAttachment)) <> '';
|
||||
");
|
||||
|
||||
migrationBuilder.Sql(@"
|
||||
IF OBJECT_ID(N'dbo.CompletionDocTemplates', N'U') IS NOT NULL
|
||||
DROP TABLE CompletionDocTemplates;
|
||||
");
|
||||
|
||||
migrationBuilder.Sql(@"
|
||||
IF COL_LENGTH('workOrderAttachments', 'Category') IS NOT NULL
|
||||
ALTER TABLE workOrderAttachments DROP COLUMN Category;
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class Phase1_BoardIndexes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
3046
Data.SeaHavenIndustries/Migrations/20260709170202_Phase5_DomainEvents.Designer.cs
generated
Normal file
3046
Data.SeaHavenIndustries/Migrations/20260709170202_Phase5_DomainEvents.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Phase5_DomainEvents : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WorkOrderWeekRolledLedgers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
WorkOrderId = table.Column<int>(type: "int", nullable: false),
|
||||
SourceWeekStart = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
ProcessedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CorrelationId = table.Column<string>(type: "nvarchar(max)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WorkOrderWeekRolledLedgers", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_WorkOrderWeekRolledLedgers_workOrders_WorkOrderId",
|
||||
column: x => x.WorkOrderId,
|
||||
principalTable: "workOrders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WorkOrderWeekRolledLedgers_WorkOrderId_SourceWeekStart",
|
||||
table: "WorkOrderWeekRolledLedgers",
|
||||
columns: new[] { "WorkOrderId", "SourceWeekStart" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "WorkOrderWeekRolledLedgers");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260713120000_Phase7_ExternalWorkOrderIdUnique")]
|
||||
public partial class Phase7_ExternalWorkOrderIdUnique : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Idempotent: column may already be nvarchar(450) in some environments.
|
||||
migrationBuilder.Sql(@"
|
||||
IF COL_LENGTH('workOrders', 'ExternalWorkOrderId') IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM sys.columns c
|
||||
INNER JOIN sys.types t ON c.user_type_id = t.user_type_id
|
||||
WHERE c.object_id = OBJECT_ID(N'dbo.workOrders')
|
||||
AND c.name = N'ExternalWorkOrderId'
|
||||
AND t.name = N'nvarchar'
|
||||
AND c.max_length = -1)
|
||||
BEGIN
|
||||
ALTER TABLE workOrders ALTER COLUMN ExternalWorkOrderId nvarchar(450) NULL;
|
||||
END
|
||||
");
|
||||
|
||||
migrationBuilder.Sql(@"
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM sys.indexes
|
||||
WHERE name = N'IX_workOrders_ExternalWorkOrderId'
|
||||
AND object_id = OBJECT_ID(N'dbo.workOrders'))
|
||||
BEGIN
|
||||
CREATE UNIQUE INDEX IX_workOrders_ExternalWorkOrderId
|
||||
ON workOrders (ExternalWorkOrderId)
|
||||
WHERE [ExternalWorkOrderId] IS NOT NULL AND [ExternalWorkOrderId] <> '';
|
||||
END
|
||||
");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM sys.indexes
|
||||
WHERE name = N'IX_workOrders_ExternalWorkOrderId'
|
||||
AND object_id = OBJECT_ID(N'dbo.workOrders'))
|
||||
DROP INDEX IX_workOrders_ExternalWorkOrderId ON workOrders;
|
||||
");
|
||||
|
||||
migrationBuilder.Sql(@"
|
||||
IF COL_LENGTH('workOrders', 'ExternalWorkOrderId') IS NOT NULL
|
||||
ALTER TABLE workOrders ALTER COLUMN ExternalWorkOrderId nvarchar(max) NULL;
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260715160000_WoServiceNotesStructuredFields")]
|
||||
public partial class WoServiceNotesStructuredFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PocName",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(200)",
|
||||
maxLength: 200,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PocPhone",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(50)",
|
||||
maxLength: 50,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PocNotes",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(2000)",
|
||||
maxLength: 2000,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ExtraServices",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(2000)",
|
||||
maxLength: 2000,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ServiceNotes",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(4000)",
|
||||
maxLength: 4000,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TechPhone",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(50)",
|
||||
maxLength: 50,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "VendorNotes",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(2000)",
|
||||
maxLength: 2000,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TechPhone",
|
||||
table: "Dispatches",
|
||||
type: "nvarchar(50)",
|
||||
maxLength: 50,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "VendorNotes",
|
||||
table: "Dispatches",
|
||||
type: "nvarchar(2000)",
|
||||
maxLength: 2000,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PocName",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PocPhone",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PocNotes",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ExtraServices",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ServiceNotes",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TechPhone",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "VendorNotes",
|
||||
table: "workOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TechPhone",
|
||||
table: "Dispatches");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "VendorNotes",
|
||||
table: "Dispatches");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260715180000_WoFlagColor")]
|
||||
public partial class WoFlagColor : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "FlagColor",
|
||||
table: "workOrders",
|
||||
type: "nvarchar(7)",
|
||||
maxLength: 7,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "FlagColor",
|
||||
table: "workOrders");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260717140000_AddEventsCalendarTable")]
|
||||
public partial class AddEventsCalendarTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Events entity existed in the model/DbSet without a discoverable migration.
|
||||
migrationBuilder.Sql(@"
|
||||
IF OBJECT_ID(N'dbo.Events', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE Events (
|
||||
Id int NOT NULL IDENTITY(1,1),
|
||||
Title nvarchar(100) NOT NULL,
|
||||
Description nvarchar(max) NULL,
|
||||
Location nvarchar(150) NULL,
|
||||
RecordID int NULL,
|
||||
RecordName nvarchar(max) NULL,
|
||||
RecordTargetType nvarchar(max) NULL,
|
||||
ReminderMinutes int NOT NULL,
|
||||
EventColor nvarchar(20) NOT NULL,
|
||||
StartDate datetime2 NOT NULL,
|
||||
StartTime nvarchar(10) NULL,
|
||||
EndDate datetime2 NOT NULL,
|
||||
EndTime nvarchar(10) NULL,
|
||||
AllDay bit NOT NULL,
|
||||
IsDeleted bit NULL,
|
||||
createdby nvarchar(max) NULL,
|
||||
DeleterUserId nvarchar(max) NULL,
|
||||
DeletionTime datetime2 NULL,
|
||||
CreatedDate datetime2 NULL,
|
||||
LastModificationTime datetime2 NULL,
|
||||
LastModifierUserId int NULL,
|
||||
CONSTRAINT PK_Events PRIMARY KEY (Id)
|
||||
);
|
||||
END
|
||||
");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"
|
||||
IF OBJECT_ID(N'dbo.Events', N'U') IS NOT NULL
|
||||
DROP TABLE Events;
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -186,6 +186,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Color")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
|
@ -209,6 +212,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("FirstName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Initials")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
|
|
@ -435,6 +441,60 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.ToTable("Comments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.CompletionDocTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DeleterUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ServiceKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("TemplateUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("WorkOrderType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ServiceKey", "IsActive");
|
||||
|
||||
b.ToTable("CompletionDocTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.ContactDetails", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -601,32 +661,11 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DeleterUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Departments");
|
||||
|
|
@ -688,15 +727,28 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("ReplyToAddress")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<DateTime?>("ScheduledDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("TechPhone")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<int>("VendorId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("VendorNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<DateTime?>("VerifiedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -1111,36 +1163,15 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DeleterUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("EmailTypeId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("EmployeeId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool?>("IsPrimary")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmployeeId");
|
||||
|
|
@ -1178,6 +1209,86 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.ToTable("EmployeePhones");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.Events", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("AllDay")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DeleterUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("EndTime")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<string>("EventColor")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("nvarchar(150)");
|
||||
|
||||
b.Property<int?>("RecordID")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("RecordName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RecordTargetType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("ReminderMinutes")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("StartTime")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.FollowUps", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -1279,32 +1390,11 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DeleterUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobTitles");
|
||||
|
|
@ -1318,21 +1408,15 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("AccountId")
|
||||
.HasColumnType("int");
|
||||
b.Property<string>("Address1")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Address")
|
||||
b.Property<string>("Address2")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("City")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Contact")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ContactEmail")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -1342,6 +1426,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
|
|
@ -1360,7 +1447,7 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("State")
|
||||
|
|
@ -1372,7 +1459,7 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Title")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ZipCode")
|
||||
b.Property<string>("Zip")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
|
|
@ -1380,8 +1467,6 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Locations");
|
||||
});
|
||||
|
||||
|
|
@ -1573,32 +1658,11 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DeleterUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Regions");
|
||||
|
|
@ -1931,12 +1995,18 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("AfterPhotoIssue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateOnly?>("AssignDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("AssignTo")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Attachments")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("AvettaTask")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("BeforPhotoAttachment")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
@ -1946,6 +2016,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Building")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("CarriedOver")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("CompletedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -1967,14 +2040,27 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("DocStatus")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("DueDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExternalWorkOrderId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
.HasMaxLength(450)
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ExtraServices")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<string>("FlagColor")
|
||||
.HasMaxLength(7)
|
||||
.HasColumnType("nvarchar(7)");
|
||||
|
||||
b.Property<string>("InternalWONumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
.HasMaxLength(450)
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
|
@ -1985,24 +2071,76 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("LegacyStatus")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("LifecycleStatus")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("LocationId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("OperationalFlags")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateOnly?>("OriginalDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly?>("OriginalWeek")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("PO")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PocName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("PocNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<string>("PocPhone")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<int?>("PrimaryDispatchId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Problem")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("RescheduleCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<bool?>("ScheduleWeekOnly")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("ScheduledDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ScheduledEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ScheduledStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Service")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ServiceNotes")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("nvarchar(4000)");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
@ -2016,7 +2154,8 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("SiteCode")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
.HasMaxLength(450)
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
|
@ -2033,12 +2172,26 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("TT")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateOnly?>("TargetWeek")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("TechPhone")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Trade")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<decimal?>("VendorNTE")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<string>("VendorNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<int?>("WorkOrderType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("WorkerOrderNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
@ -2055,8 +2208,29 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
b.HasIndex("AssignTo");
|
||||
|
||||
b.HasIndex("ExternalWorkOrderId")
|
||||
.IsUnique()
|
||||
.HasFilter("[ExternalWorkOrderId] IS NOT NULL AND [ExternalWorkOrderId] <> ''");
|
||||
|
||||
b.HasIndex("InternalWONumber")
|
||||
.HasFilter("[istemplate] = 0");
|
||||
|
||||
b.HasIndex("LocationId");
|
||||
|
||||
b.HasIndex("PrimaryDispatchId");
|
||||
|
||||
b.HasIndex("ScheduledDate");
|
||||
|
||||
b.HasIndex("SiteCode")
|
||||
.HasFilter("[istemplate] = 0");
|
||||
|
||||
b.HasIndex("WorkOrderType");
|
||||
|
||||
b.HasIndex("AssignTo", "ScheduledDate");
|
||||
|
||||
b.HasIndex("LifecycleStatus", "ScheduledDate")
|
||||
.HasFilter("[istemplate] = 0");
|
||||
|
||||
b.ToTable("workOrders");
|
||||
});
|
||||
|
||||
|
|
@ -2071,6 +2245,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Attachments")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("Category")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -2113,9 +2290,21 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Action")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ActorType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("CorrelationId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("DispatchId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("FieldName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
@ -2213,6 +2402,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("WorkorderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
|
|
@ -2228,6 +2420,64 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.ToTable("WorkOrderContacts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderFieldLock", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("FieldName")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTime>("LockedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LockedByUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("WorkOrderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkOrderId", "FieldName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("WorkOrderFieldLocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderWeekRolledLedger", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("CorrelationId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("ProcessedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateOnly>("SourceWeekStart")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<int>("WorkOrderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkOrderId", "SourceWeekStart")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("WorkOrderWeekRolledLedgers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
|
|
@ -2457,8 +2707,7 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder")
|
||||
.WithMany("Dispatches")
|
||||
.HasForeignKey("WorkOrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
.HasForeignKey("WorkOrderId");
|
||||
|
||||
b.Navigation("Vendor");
|
||||
|
||||
|
|
@ -2612,16 +2861,6 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Navigation("WorkOrder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.Locations", b =>
|
||||
{
|
||||
b.HasOne("Data.SeaHavenIndustries.Accounts", "Account")
|
||||
.WithMany()
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.PMSchedules", b =>
|
||||
{
|
||||
b.HasOne("Data.SeaHavenIndustries.Assets", "Asset")
|
||||
|
|
@ -2731,9 +2970,16 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
.HasForeignKey("LocationId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("Data.SeaHavenIndustries.Dispatch", "PrimaryDispatch")
|
||||
.WithMany()
|
||||
.HasForeignKey("PrimaryDispatchId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("AssignToUser");
|
||||
|
||||
b.Navigation("Locations");
|
||||
|
||||
b.Navigation("PrimaryDispatch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderAttachments", b =>
|
||||
|
|
@ -2798,6 +3044,28 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Navigation("WorkOrder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderFieldLock", b =>
|
||||
{
|
||||
b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder")
|
||||
.WithMany("FieldLocks")
|
||||
.HasForeignKey("WorkOrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("WorkOrder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderWeekRolledLedger", b =>
|
||||
{
|
||||
b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder")
|
||||
.WithMany()
|
||||
.HasForeignKey("WorkOrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("WorkOrder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
|
|
@ -2943,6 +3211,8 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
b.Navigation("Dispatches");
|
||||
|
||||
b.Navigation("FieldLocks");
|
||||
|
||||
b.Navigation("Quotes");
|
||||
|
||||
b.Navigation("WorkOrderCategories");
|
||||
|
|
|
|||
13
Data.SeaHavenIndustries/Models/CompletionDocTemplate.cs
Normal file
13
Data.SeaHavenIndustries/Models/CompletionDocTemplate.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace Data.SeaHavenIndustries
|
||||
{
|
||||
public class CompletionDocTemplate : FullAuditEntity
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string ServiceKey { get; set; } = "";
|
||||
public WorkOrderType? WorkOrderType { get; set; }
|
||||
public string TemplateUrl { get; set; } = "";
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Data.SeaHavenIndustries
|
||||
|
|
@ -26,6 +27,17 @@ namespace Data.SeaHavenIndustries
|
|||
public string? AcceptToken { get; set; }
|
||||
public string? VerifiedBy { get; set; }
|
||||
public DateTime? VerifiedAt { get; set; }
|
||||
|
||||
/// <summary>Tech/crew phone for this dispatch (overrides Vendor.Phone on board).</summary>
|
||||
[MaxLength(50)]
|
||||
public string? TechPhone { get; set; }
|
||||
/// <summary>Instructions for the vendor for this appointment/dispatch.</summary>
|
||||
[MaxLength(2000)]
|
||||
public string? VendorNotes { get; set; }
|
||||
|
||||
[Timestamp]
|
||||
public byte[]? RowVersion { get; set; }
|
||||
|
||||
public ICollection<Comments>? Comments { get; set; }
|
||||
public ICollection<DispatchWorkOrder>? DispatchWorkOrders { get; set; }
|
||||
public ICollection<DispatchChecklistItem>? ChecklistItems { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace Data.SeaHavenIndustries
|
||||
{
|
||||
public class WorkOrderAttachments : FullAuditEntity
|
||||
{
|
||||
public string? Attachments { get; set; }
|
||||
public WorkOrderMediaCategory? Category { get; set; }
|
||||
public int? WorkorderId { get; set; }
|
||||
[ForeignKey(nameof(WorkorderId))]
|
||||
public virtual WorkOrder? WorkOrder { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ namespace Data.SeaHavenIndustries
|
|||
public string? OldValue { get; set; }
|
||||
public string? NewValue { get; set; }
|
||||
public string? Action { get; set; }
|
||||
public string? EventType { get; set; }
|
||||
public string? ActorType { get; set; }
|
||||
public int? DispatchId { get; set; }
|
||||
public string? CorrelationId { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@ namespace Data.SeaHavenIndustries
|
|||
public int? WorkorderId { get; set; }
|
||||
[ForeignKey(nameof(WorkorderId))]
|
||||
public virtual WorkOrder? WorkOrder { get; set; }
|
||||
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
Data.SeaHavenIndustries/Models/WorkOrderFieldLock.cs
Normal file
18
Data.SeaHavenIndustries/Models/WorkOrderFieldLock.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Data.SeaHavenIndustries
|
||||
{
|
||||
/// <summary>
|
||||
/// ManualEditFlag — blocks Sync overwrite after first SHOC edit per field.
|
||||
/// </summary>
|
||||
public class WorkOrderFieldLock
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int WorkOrderId { get; set; }
|
||||
[ForeignKey(nameof(WorkOrderId))]
|
||||
public virtual WorkOrder? WorkOrder { get; set; }
|
||||
public string FieldName { get; set; } = "";
|
||||
public DateTime LockedAt { get; set; }
|
||||
public string? LockedByUserId { get; set; }
|
||||
}
|
||||
}
|
||||
15
Data.SeaHavenIndustries/Models/WorkOrderWeekRolledLedger.cs
Normal file
15
Data.SeaHavenIndustries/Models/WorkOrderWeekRolledLedger.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Data.SeaHavenIndustries
|
||||
{
|
||||
public class WorkOrderWeekRolledLedger
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int WorkOrderId { get; set; }
|
||||
[ForeignKey(nameof(WorkOrderId))]
|
||||
public virtual WorkOrder? WorkOrder { get; set; }
|
||||
public DateOnly SourceWeekStart { get; set; }
|
||||
public DateTime ProcessedAt { get; set; }
|
||||
public string CorrelationId { get; set; } = "";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +1,101 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace Data.SeaHavenIndustries
|
||||
{
|
||||
public class WorkOrder : FullAuditEntity
|
||||
{
|
||||
// --- Core slice ---
|
||||
[MaxLength(450)]
|
||||
public string? InternalWONumber { get; set; }
|
||||
[MaxLength(450)]
|
||||
public string? ExternalWorkOrderId { get; set; }
|
||||
public string? WorkerOrderNumber { get; set; }
|
||||
public string? WorkerOrderTitle { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Customer { get; set; }
|
||||
[MaxLength(450)]
|
||||
public string? SiteCode { get; set; }
|
||||
public string? Building { get; set; }
|
||||
public string? Severity { get; set; }
|
||||
public DateTime? DateReported { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
public string? AssignTo { get; set; }
|
||||
[ForeignKey(nameof(AssignTo))]
|
||||
public virtual ApplicationUser? AssignToUser { get; set; }
|
||||
public int? LocationId { get; set; }
|
||||
[ForeignKey(nameof(LocationId))]
|
||||
public virtual Locations? Locations { get; set; }
|
||||
public LifecycleStatus? LifecycleStatus { get; set; }
|
||||
public string? LegacyStatus { get; set; }
|
||||
public WorkOrderType? WorkOrderType { get; set; }
|
||||
/// <summary>SHOC board "Service" column (PM type label). Not Trade/Problem.</summary>
|
||||
[MaxLength(128)]
|
||||
public string? Service { get; set; }
|
||||
public int? PrimaryDispatchId { get; set; }
|
||||
[ForeignKey(nameof(PrimaryDispatchId))]
|
||||
public virtual Dispatch? PrimaryDispatch { get; set; }
|
||||
|
||||
// --- Service & Notes / POC (structured create) ---
|
||||
[MaxLength(200)]
|
||||
public string? PocName { get; set; }
|
||||
[MaxLength(50)]
|
||||
public string? PocPhone { get; set; }
|
||||
[MaxLength(2000)]
|
||||
public string? PocNotes { get; set; }
|
||||
/// <summary>JSON array of extra service labels beyond primary (Trade).</summary>
|
||||
[MaxLength(2000)]
|
||||
public string? ExtraServices { get; set; }
|
||||
[MaxLength(4000)]
|
||||
public string? ServiceNotes { get; set; }
|
||||
/// <summary>Tech/crew phone when no primary dispatch; otherwise prefer Dispatch.TechPhone.</summary>
|
||||
[MaxLength(50)]
|
||||
public string? TechPhone { get; set; }
|
||||
/// <summary>Vendor instructions when no primary dispatch; otherwise prefer Dispatch.VendorNotes.</summary>
|
||||
[MaxLength(2000)]
|
||||
public string? VendorNotes { get; set; }
|
||||
|
||||
// --- Scheduling slice ---
|
||||
public DateTime? ScheduledDate { get; set; }
|
||||
public DateTime? ScheduledStart { get; set; }
|
||||
public DateTime? ScheduledEnd { get; set; }
|
||||
public DateOnly? TargetWeek { get; set; }
|
||||
public bool? ScheduleWeekOnly { get; set; }
|
||||
|
||||
// --- Tracking slice (set-once) ---
|
||||
public DateOnly? OriginalWeek { get; set; }
|
||||
public DateOnly? OriginalDate { get; set; }
|
||||
|
||||
// --- Analytics slice ---
|
||||
public int RescheduleCount { get; set; }
|
||||
public int CarriedOver { get; set; }
|
||||
public OperationalFlags OperationalFlags { get; set; }
|
||||
|
||||
// --- Completion slice ---
|
||||
public DocStatus? DocStatus { get; set; }
|
||||
|
||||
/// <summary>Board flag color (#RRGGBB from fixed palette). Null = no flag.</summary>
|
||||
[MaxLength(7)]
|
||||
public string? FlagColor { get; set; }
|
||||
|
||||
// --- Legacy SQL columns (drift reconciliation) ---
|
||||
public string? AvettaTask { get; set; }
|
||||
public DateOnly? AssignDate { get; set; }
|
||||
|
||||
// --- Legacy fields (retained for Blazor / coexistence) ---
|
||||
public DateTime? DateReported { get; set; }
|
||||
public string? SourceEmailS3Key { get; set; }
|
||||
public string? Problem { get; set; }
|
||||
public string? Trade { get; set; }
|
||||
public string? SubTrade { get; set; }
|
||||
public decimal? VendorNTE { get; set; }
|
||||
public DateTime? ScheduledDate { get; set; }
|
||||
public DateTime? CompletedDate { get; set; }
|
||||
public string? Source { get; set; }
|
||||
public bool? istemplate { get; set; }
|
||||
public string? PO { get; set; }
|
||||
public string? TT { get; set; }
|
||||
//public int? ContactId { get; set; }
|
||||
//[ForeignKey(nameof(ContactId))]
|
||||
//public virtual Contacts? POC { get; set; }
|
||||
public string? AssignTo { get; set; }
|
||||
[ForeignKey(nameof(AssignTo))]
|
||||
public virtual ApplicationUser? AssignToUser { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
public string? Priority { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public int? LocationId { get; set; }
|
||||
[ForeignKey(nameof(LocationId))]
|
||||
public virtual Locations? Locations { get; set; }
|
||||
//public int? CategoryId { get; set; }
|
||||
//[ForeignKey(nameof(CategoryId))]
|
||||
//public virtual Category? Category { get; set; }
|
||||
public string? Attachments { get; set; }
|
||||
public string? BeforPhotoIssue { get; set; }
|
||||
public string? BeforPhotoAttachment { get; set; }
|
||||
|
|
@ -51,11 +104,16 @@ namespace Data.SeaHavenIndustries
|
|||
public string? SignOffName { get; set; }
|
||||
public string? SignOffAttachment { get; set; }
|
||||
public string? SignOffSignature { get; set; }
|
||||
|
||||
[Timestamp]
|
||||
public byte[]? RowVersion { get; set; }
|
||||
|
||||
public ICollection<Comments>? Comments { get; set; }
|
||||
public ICollection<Quotes>? Quotes { get; set; }
|
||||
public ICollection<WorkOrderContacts>? WorkOrderContacts { get; set; }
|
||||
public ICollection<WorkOrderAttachments>? workOrderAttachments { get; set; }
|
||||
public ICollection<WorkOrderCategories>? WorkOrderCategories { get; set; }
|
||||
public ICollection<Dispatch>? Dispatches { get; set; }
|
||||
public ICollection<WorkOrderFieldLock>? FieldLocks { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
SeaHaven.DataServices/Board/WorkOrderBoardConstants.cs
Normal file
7
SeaHaven.DataServices/Board/WorkOrderBoardConstants.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace SeaHaven.DataServices.Board
|
||||
{
|
||||
public static class WorkOrderBoardConstants
|
||||
{
|
||||
public const int ClientSideThreshold = 300;
|
||||
}
|
||||
}
|
||||
10
SeaHaven.DataServices/DTOs/WorkOrderBoardQuery.cs
Normal file
10
SeaHaven.DataServices/DTOs/WorkOrderBoardQuery.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace SeaHaven.DataServices.DTOs
|
||||
{
|
||||
public record WorkOrderBoardQuery(
|
||||
DateOnly ScheduledFrom,
|
||||
DateOnly ScheduledTo,
|
||||
string? Search,
|
||||
IReadOnlyList<string>? AssigneeIds,
|
||||
IReadOnlyList<string>? DbStatuses,
|
||||
int? LocationId);
|
||||
}
|
||||
20
SeaHaven.DataServices/DTOs/WorkOrderBoardRowData.cs
Normal file
20
SeaHaven.DataServices/DTOs/WorkOrderBoardRowData.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
namespace SeaHaven.DataServices.DTOs
|
||||
{
|
||||
public record WorkOrderBoardRowData(
|
||||
int Id,
|
||||
string? WoNumber,
|
||||
string? SiteCode,
|
||||
string? LocationLabel,
|
||||
DateTime? ScheduledDate,
|
||||
DateTime? ScheduledStart,
|
||||
DateTime? DueDate,
|
||||
string? WorkOrderType,
|
||||
string? ServiceType,
|
||||
string? AssigneeId,
|
||||
string? AssigneeName,
|
||||
string? VendorCompany,
|
||||
string? VendorTechnician,
|
||||
string? Status,
|
||||
string? PocName,
|
||||
string? PocPhone);
|
||||
}
|
||||
9
SeaHaven.DataServices/Exceptions/BoardQueryException.cs
Normal file
9
SeaHaven.DataServices/Exceptions/BoardQueryException.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace SeaHaven.DataServices.Exceptions
|
||||
{
|
||||
public class BoardQueryException : Exception
|
||||
{
|
||||
public BoardQueryException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
127
SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs
Normal file
127
SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Helpers
|
||||
{
|
||||
public static class WorkOrderBoardProjection
|
||||
{
|
||||
public static async Task<IReadOnlyList<WorkOrderBoardRawRow>> ProjectRowsAsync(
|
||||
IQueryable<WorkOrder> 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,
|
||||
WoPocName = w.PocName,
|
||||
WoPocPhone = w.PocPhone,
|
||||
WoPocNotes = w.PocNotes,
|
||||
ContactPoc = w.WorkOrderContacts!
|
||||
.OrderBy(c => c.Id)
|
||||
.Select(c => new
|
||||
{
|
||||
Name = c.POC != null
|
||||
? ((c.POC.FirstName ?? "") + " " + (c.POC.LastName ?? "")).Trim()
|
||||
: null,
|
||||
c.POC!.PhoneNumber,
|
||||
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,
|
||||
DispatchTechPhone = w.PrimaryDispatch != null ? w.PrimaryDispatch.TechPhone : null,
|
||||
WoTechPhone = w.TechPhone,
|
||||
VendorPhone = w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null
|
||||
? w.PrimaryDispatch.Vendor.Phone
|
||||
: null,
|
||||
DispatchApptDate = w.PrimaryDispatch != null ? w.PrimaryDispatch.ScheduledDate : null,
|
||||
w.Trade,
|
||||
w.Problem,
|
||||
w.ServiceNotes,
|
||||
w.ExtraServices,
|
||||
w.DocStatus,
|
||||
w.FlagColor,
|
||||
w.PrimaryDispatchId,
|
||||
w.RowVersion,
|
||||
DispatchRowVersion = w.PrimaryDispatch != null ? w.PrimaryDispatch.RowVersion : null
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return rows.Select(w =>
|
||||
{
|
||||
var pocName = !string.IsNullOrWhiteSpace(w.WoPocName) ? w.WoPocName : w.ContactPoc?.Name;
|
||||
var pocPhone = !string.IsNullOrWhiteSpace(w.WoPocPhone) ? w.WoPocPhone : w.ContactPoc?.PhoneNumber;
|
||||
var pocNotes = !string.IsNullOrWhiteSpace(w.WoPocNotes) ? w.WoPocNotes : w.ContactPoc?.Notes;
|
||||
var techPhone = !string.IsNullOrWhiteSpace(w.DispatchTechPhone)
|
||||
? w.DispatchTechPhone
|
||||
: (!string.IsNullOrWhiteSpace(w.WoTechPhone) ? w.WoTechPhone : w.VendorPhone);
|
||||
|
||||
return new WorkOrderBoardRawRow(
|
||||
w.Id,
|
||||
w.InternalWONumber,
|
||||
w.RescheduleCount,
|
||||
w.CarriedOver,
|
||||
w.WorkOrderType,
|
||||
w.SiteCode,
|
||||
w.LocationName,
|
||||
pocName,
|
||||
pocPhone,
|
||||
pocNotes,
|
||||
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,
|
||||
techPhone,
|
||||
w.DispatchApptDate,
|
||||
w.Trade ?? w.Problem,
|
||||
w.Problem,
|
||||
w.ServiceNotes,
|
||||
w.ExtraServices,
|
||||
w.DocStatus,
|
||||
w.FlagColor,
|
||||
w.PrimaryDispatchId,
|
||||
w.RowVersion,
|
||||
w.DispatchRowVersion,
|
||||
isUnscheduled);
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
174
SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs
Normal file
174
SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.DataServices.Helpers
|
||||
{
|
||||
public static class WorkOrderBoardQueryFilters
|
||||
{
|
||||
public static IQueryable<WorkOrder> ApplyBaseScope(IQueryable<WorkOrder> query)
|
||||
=> query.Where(w => w.istemplate != true && (w.IsDeleted != true || w.IsDeleted == null));
|
||||
|
||||
public static IQueryable<WorkOrder> ApplyDispatcherFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<string>? 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FE sends <see cref="WorkOrderType.Other"/> (99) as the overdue/isPastDue sentinel — not a real type filter.
|
||||
/// </summary>
|
||||
public static IQueryable<WorkOrder> ApplyTypeAndOverdueFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<WorkOrderType>? types,
|
||||
DateTime? utcNow = null)
|
||||
{
|
||||
if (types == null || types.Count == 0)
|
||||
return query;
|
||||
|
||||
var today = (utcNow ?? DateTime.UtcNow).Date;
|
||||
var includeOverdue = types.Contains(WorkOrderType.Other);
|
||||
var typeFilters = types.Where(t => t != WorkOrderType.Other).ToList();
|
||||
|
||||
// Keep past-due predicate inline — EF cannot translate local method calls.
|
||||
if (includeOverdue && typeFilters.Count == 0)
|
||||
{
|
||||
return query.Where(w =>
|
||||
w.ScheduledDate != null
|
||||
&& w.ScheduledDate.Value.Date < today
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled);
|
||||
}
|
||||
|
||||
if (includeOverdue)
|
||||
{
|
||||
return query.Where(w =>
|
||||
(w.ScheduledDate != null
|
||||
&& w.ScheduledDate.Value.Date < today
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled)
|
||||
|| (w.WorkOrderType != null && typeFilters.Contains(w.WorkOrderType.Value)));
|
||||
}
|
||||
|
||||
return query.Where(w => w.WorkOrderType != null && typeFilters.Contains(w.WorkOrderType.Value));
|
||||
}
|
||||
|
||||
public static IQueryable<WorkOrder> ApplyTypeFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<WorkOrderType>? types)
|
||||
=> ApplyTypeAndOverdueFilter(query, types);
|
||||
|
||||
public static IQueryable<WorkOrder> ApplySiteFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<string>? 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<WorkOrder> ApplyStatusFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<LifecycleStatus>? statuses)
|
||||
{
|
||||
if (statuses == null || statuses.Count == 0)
|
||||
return query;
|
||||
|
||||
return query.Where(w => w.LifecycleStatus != null && statuses.Contains(w.LifecycleStatus.Value));
|
||||
}
|
||||
|
||||
public static IQueryable<WorkOrder> ApplyDocStatusFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<DocStatus>? docStatuses)
|
||||
{
|
||||
if (docStatuses == null || docStatuses.Count == 0)
|
||||
return query;
|
||||
|
||||
return query.Where(w => w.DocStatus != null && docStatuses.Contains(w.DocStatus.Value));
|
||||
}
|
||||
|
||||
public static IQueryable<WorkOrder> ApplyVendorFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<int>? vendorIds)
|
||||
{
|
||||
if (vendorIds == null || vendorIds.Count == 0)
|
||||
return query;
|
||||
|
||||
return query.Where(w =>
|
||||
w.PrimaryDispatch != null && vendorIds.Contains(w.PrimaryDispatch.VendorId));
|
||||
}
|
||||
|
||||
public static IQueryable<WorkOrder> ApplyPmTypeFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<string>? 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<WorkOrder> ApplyDateRangeFilter(
|
||||
IQueryable<WorkOrder> 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)
|
||||
|| (w.ScheduledDate == null
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled));
|
||||
}
|
||||
}
|
||||
}
|
||||
133
SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs
Normal file
133
SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.DataServices.Helpers
|
||||
{
|
||||
public static class WorkOrderBoardSearchFilter
|
||||
{
|
||||
private static readonly Regex PartialDatePattern = new(
|
||||
@"^\d{1,2}/\d{1,2}$",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
public static string? NormalizeSearch(string? search)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(search))
|
||||
return null;
|
||||
|
||||
var trimmed = search.Trim();
|
||||
return trimmed.Length < 2 ? null : trimmed;
|
||||
}
|
||||
|
||||
public static IQueryable<WorkOrder> Apply(IQueryable<WorkOrder> query, string? search)
|
||||
{
|
||||
var normalized = NormalizeSearch(search);
|
||||
if (normalized == null)
|
||||
return query;
|
||||
|
||||
var s = normalized.ToLower();
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var exactDate = TryParseExactDate(normalized);
|
||||
var hasExactDate = exactDate.HasValue;
|
||||
var exactDateValue = exactDate?.Date ?? default;
|
||||
|
||||
var partialMonthDay = TryParsePartialMonthDay(normalized);
|
||||
var hasPartialMonthDay = partialMonthDay.HasValue;
|
||||
var partialMonth = partialMonthDay?.Month ?? 0;
|
||||
var partialDay = partialMonthDay?.Day ?? 0;
|
||||
|
||||
// Keep predicates inline — EF Core cannot translate local method calls.
|
||||
return query.Where(w =>
|
||||
(w.SiteCode ?? "").ToLower().Contains(s)
|
||||
|| (w.InternalWONumber ?? "").ToLower().Contains(s)
|
||||
|| (w.WorkerOrderNumber ?? "").ToLower().Contains(s)
|
||||
|| (w.WorkerOrderTitle ?? "").ToLower().Contains(s)
|
||||
|| (w.Description ?? "").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.Trade ?? "").ToLower().Contains(s)
|
||||
|| (w.Problem ?? "").ToLower().Contains(s)
|
||||
|| (w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null && (w.PrimaryDispatch.Vendor.CompanyName ?? "").ToLower().Contains(s))
|
||||
|| (w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null && (w.PrimaryDispatch.Vendor.ContactName ?? "").ToLower().Contains(s))
|
||||
|| (w.Status ?? "").ToLower().Contains(s)
|
||||
|| (w.WorkOrderType == WorkOrderType.PO && "po".Contains(s))
|
||||
|| (w.WorkOrderType == WorkOrderType.PM && "pm".Contains(s))
|
||||
|| (w.WorkOrderType == WorkOrderType.Emergency && "emergency".Contains(s))
|
||||
|| (w.WorkOrderType == WorkOrderType.Project && "project".Contains(s))
|
||||
|| (w.WorkOrderType == WorkOrderType.Inspection && "inspection".Contains(s))
|
||||
|| (w.WorkOrderType == WorkOrderType.Reactive && "reactive".Contains(s))
|
||||
|| (w.WorkOrderType == WorkOrderType.AddOn && ("add-on".Contains(s) || "addon".Contains(s)))
|
||||
|| (w.WorkOrderType == WorkOrderType.Other && "other".Contains(s))
|
||||
|| ("overdue".Contains(s)
|
||||
&& w.ScheduledDate != null
|
||||
&& w.ScheduledDate.Value.Date < today
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled)
|
||||
|| (w.LifecycleStatus == LifecycleStatus.Incomplete && "incomplete".Contains(s))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.Pending && ("pending".Contains(s) || "on hold".Contains(s) || "onhold".Contains(s)))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.Scheduled && "scheduled".Contains(s))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.EnRoute && ("en route".Contains(s) || "enroute".Contains(s)))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.OnSite && ("on site".Contains(s) || "onsite".Contains(s)))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.InProgress && ("in progress".Contains(s) || "inprogress".Contains(s)))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.Completed && ("completed".Contains(s) || "complete".Contains(s) || "done".Contains(s) || "closed".Contains(s)))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.Rescheduled && "rescheduled".Contains(s))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.Canceled && ("canceled".Contains(s) || "cancelled".Contains(s)))
|
||||
|| (w.LifecycleStatus == LifecycleStatus.PendingQuote && ("pending quote".Contains(s) || "pendingquote".Contains(s) || "pending approval".Contains(s) || "pendingapproval".Contains(s)))
|
||||
|| (hasExactDate
|
||||
&& ((w.ScheduledDate != null && w.ScheduledDate.Value.Date == exactDateValue)
|
||||
|| (w.DueDate != null && w.DueDate.Value.Date == exactDateValue)))
|
||||
|| (hasPartialMonthDay
|
||||
&& ((w.ScheduledDate != null
|
||||
&& w.ScheduledDate.Value.Month == partialMonth
|
||||
&& w.ScheduledDate.Value.Day == partialDay)
|
||||
|| (w.DueDate != null
|
||||
&& w.DueDate.Value.Month == partialMonth
|
||||
&& w.DueDate.Value.Day == partialDay))));
|
||||
}
|
||||
|
||||
internal static DateTime? TryParseExactDate(string search)
|
||||
{
|
||||
if (PartialDatePattern.IsMatch(search))
|
||||
return null;
|
||||
|
||||
var formats = new[]
|
||||
{
|
||||
"yyyy-MM-dd",
|
||||
"MM/dd/yyyy",
|
||||
"M/d/yyyy",
|
||||
"MM/dd/yy",
|
||||
"M/d/yy"
|
||||
};
|
||||
|
||||
if (DateTime.TryParseExact(search, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed))
|
||||
return parsed.Date;
|
||||
|
||||
if (DateTime.TryParse(search, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed))
|
||||
return parsed.Date;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static (int Month, int Day)? TryParsePartialMonthDay(string search)
|
||||
{
|
||||
if (!PartialDatePattern.IsMatch(search))
|
||||
return null;
|
||||
|
||||
var parts = search.Split('/');
|
||||
if (parts.Length != 2)
|
||||
return null;
|
||||
|
||||
if (!int.TryParse(parts[0], NumberStyles.None, CultureInfo.InvariantCulture, out var month))
|
||||
return null;
|
||||
|
||||
if (!int.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var day))
|
||||
return null;
|
||||
|
||||
if (month is < 1 or > 12 || day is < 1 or > 31)
|
||||
return null;
|
||||
|
||||
return (month, day);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
{
|
||||
public class CompletionDocTemplateDataService : ICompletionDocTemplateDataService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public CompletionDocTemplateDataService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CompletionDocTemplateRow>> GetActiveAsync(string? serviceKey, WorkOrderType? workOrderType)
|
||||
{
|
||||
var query = _context.CompletionDocTemplates
|
||||
.AsNoTracking()
|
||||
.Where(t => t.IsActive && t.IsDeleted != true);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(serviceKey))
|
||||
query = query.Where(t => t.ServiceKey == serviceKey);
|
||||
|
||||
if (workOrderType.HasValue)
|
||||
query = query.Where(t => t.WorkOrderType == null || t.WorkOrderType == workOrderType);
|
||||
|
||||
return await query
|
||||
.OrderBy(t => t.Name)
|
||||
.Select(MapRow)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<CompletionDocTemplateRow?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.CompletionDocTemplates
|
||||
.AsNoTracking()
|
||||
.Where(t => t.Id == id && t.IsDeleted != true)
|
||||
.Select(MapRow)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<CompletionDocTemplateRow?> ResolveForWorkOrderAsync(string? trade, WorkOrderType? workOrderType)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(trade))
|
||||
{
|
||||
var byTrade = await _context.CompletionDocTemplates
|
||||
.AsNoTracking()
|
||||
.Where(t => t.IsActive && t.IsDeleted != true && t.ServiceKey == trade)
|
||||
.OrderBy(t => t.WorkOrderType == workOrderType ? 0 : 1)
|
||||
.ThenBy(t => t.Name)
|
||||
.ThenBy(t => t.Id)
|
||||
.Select(MapRow)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (byTrade != null)
|
||||
return byTrade;
|
||||
}
|
||||
|
||||
if (!workOrderType.HasValue)
|
||||
return null;
|
||||
|
||||
return await _context.CompletionDocTemplates
|
||||
.AsNoTracking()
|
||||
.Where(t => t.IsActive && t.IsDeleted != true && t.WorkOrderType == workOrderType)
|
||||
.OrderBy(t => t.Name)
|
||||
.ThenBy(t => t.Id)
|
||||
.Select(MapRow)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<CompletionDocTemplate> CreateAsync(CompletionDocTemplate template)
|
||||
{
|
||||
template.CreatedDate = DateTime.UtcNow;
|
||||
await _context.CompletionDocTemplates.AddAsync(template);
|
||||
await _context.SaveChangesAsync();
|
||||
return template;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(CompletionDocTemplate template)
|
||||
{
|
||||
template.LastModificationTime = DateTime.UtcNow;
|
||||
_context.CompletionDocTemplates.Update(template);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id)
|
||||
{
|
||||
var entity = await _context.CompletionDocTemplates.FindAsync(id);
|
||||
if (entity == null)
|
||||
return false;
|
||||
|
||||
entity.IsDeleted = true;
|
||||
entity.DeletionTime = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly System.Linq.Expressions.Expression<Func<CompletionDocTemplate, CompletionDocTemplateRow>> MapRow =
|
||||
t => new CompletionDocTemplateRow(t.Id, t.Name, t.ServiceKey, t.WorkOrderType, t.TemplateUrl, t.IsActive);
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +60,24 @@ namespace SeaHaven.DataServices.Implementation
|
|||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SiteOptionRow>> GetSiteOptionsAsync(string? search = null)
|
||||
{
|
||||
var query = _context.Locations.AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var s = search.ToLower();
|
||||
query = query.Where(l =>
|
||||
(l.Name ?? "").ToLower().Contains(s) ||
|
||||
(l.City ?? "").ToLower().Contains(s));
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(l => l.Name)
|
||||
.Select(l => new SiteOptionRow(l.Id, l.Name, l.City, l.State))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Locations> AddAsync(Locations location)
|
||||
{
|
||||
location.CreatedDate = DateTime.Now;
|
||||
|
|
|
|||
|
|
@ -33,5 +33,27 @@ namespace SeaHaven.DataServices.Implementation
|
|||
{
|
||||
return await _context.Users.AnyAsync(u => u.Id == id);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<string, string>> GetDisplayNamesByIdsAsync(IEnumerable<string> ids)
|
||||
{
|
||||
var idList = ids
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
if (idList.Count == 0)
|
||||
return new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
var users = await _context.Users
|
||||
.AsNoTracking()
|
||||
.Where(u => idList.Contains(u.Id))
|
||||
.Select(u => new { u.Id, u.FirstName, u.LastName })
|
||||
.ToListAsync();
|
||||
|
||||
return users.ToDictionary(
|
||||
u => u.Id,
|
||||
u => $"{u.FirstName ?? ""} {u.LastName ?? ""}".Trim(),
|
||||
StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Helpers;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
{
|
||||
public class WorkOrderAdvancedSearchDataService : IWorkOrderAdvancedSearchDataService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public WorkOrderAdvancedSearchDataService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<WorkOrderAdvancedSearchResult> SearchAsync(WorkOrderAdvancedSearchQuery query)
|
||||
{
|
||||
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyDateRangeFilter(baseQuery, query.DateFrom, query.DateTo);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplySiteFilter(baseQuery, query.Sites);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyDispatcherFilter(
|
||||
baseQuery, query.Dispatchers, query.MyWorkOrders, query.CurrentUserId);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyStatusFilter(baseQuery, query.Statuses);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyPmTypeFilter(baseQuery, query.PmTypes);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyVendorFilter(baseQuery, query.VendorIds);
|
||||
baseQuery = WorkOrderBoardQueryFilters.ApplyDocStatusFilter(baseQuery, query.DocStatuses);
|
||||
|
||||
var normalizedSearch = WorkOrderBoardSearchFilter.NormalizeSearch(query.Search);
|
||||
if (normalizedSearch != null)
|
||||
baseQuery = WorkOrderBoardSearchFilter.Apply(baseQuery, normalizedSearch);
|
||||
|
||||
var totalCount = await baseQuery.CountAsync();
|
||||
|
||||
var sorted = ApplySort(baseQuery, query.SortBy, query.SortDir);
|
||||
var paged = sorted
|
||||
.Skip(query.Page * query.PageSize)
|
||||
.Take(query.PageSize);
|
||||
|
||||
var rows = await WorkOrderBoardProjection.ProjectRowsAsync(paged, isUnscheduled: false);
|
||||
return new WorkOrderAdvancedSearchResult(rows, totalCount);
|
||||
}
|
||||
|
||||
private static IQueryable<WorkOrder> ApplySort(IQueryable<WorkOrder> query, string sortBy, string sortDir)
|
||||
{
|
||||
var descending = string.Equals(sortDir, "desc", StringComparison.OrdinalIgnoreCase);
|
||||
var field = (sortBy ?? "scheduledDate").Trim().ToLowerInvariant();
|
||||
|
||||
return field switch
|
||||
{
|
||||
"wonumber" or "woNumber" => descending
|
||||
? query.OrderByDescending(w => w.InternalWONumber)
|
||||
: query.OrderBy(w => w.InternalWONumber),
|
||||
"duedate" or "dueDate" => descending
|
||||
? query.OrderByDescending(w => w.DueDate).ThenBy(w => w.InternalWONumber)
|
||||
: query.OrderBy(w => w.DueDate).ThenBy(w => w.InternalWONumber),
|
||||
_ => descending
|
||||
? query.OrderByDescending(w => w.ScheduledDate).ThenByDescending(w => w.InternalWONumber)
|
||||
: query.OrderBy(w => w.ScheduledDate).ThenBy(w => w.InternalWONumber)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
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<WorkOrderBoardQueryResult> 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
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled);
|
||||
|
||||
var scheduledTotal = await scheduledBase.CountAsync();
|
||||
|
||||
var normalizedSearch = WorkOrderBoardSearchFilter.NormalizeSearch(query.Search);
|
||||
if (normalizedSearch != null)
|
||||
{
|
||||
scheduledBase = WorkOrderBoardSearchFilter.Apply(scheduledBase, normalizedSearch);
|
||||
unscheduledBase = WorkOrderBoardSearchFilter.Apply(unscheduledBase, normalizedSearch);
|
||||
}
|
||||
|
||||
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<IReadOnlyList<DispatcherLookupRow>> 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();
|
||||
}
|
||||
|
||||
public async Task<WorkOrderBoardRawRow?> GetBoardRowByIdAsync(int workOrderId)
|
||||
{
|
||||
var query = _context.workOrders
|
||||
.AsNoTracking()
|
||||
.Where(w => w.Id == workOrderId && w.istemplate != true && (w.IsDeleted != true || w.IsDeleted == null));
|
||||
|
||||
var rows = await WorkOrderBoardProjection.ProjectRowsAsync(query, isUnscheduled: false);
|
||||
return rows.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<bool> InternalWoNumberExistsAsync(string normalizedWoNumber, int excludeWorkOrderId)
|
||||
{
|
||||
return await _context.workOrders
|
||||
.AsNoTracking()
|
||||
.AnyAsync(w =>
|
||||
w.Id != excludeWorkOrderId
|
||||
&& w.istemplate != true
|
||||
&& (w.IsDeleted != true || w.IsDeleted == null)
|
||||
&& w.InternalWONumber == normalizedWoNumber);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Helpers;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
{
|
||||
public class WorkOrderDetailDataService : IWorkOrderDetailDataService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public WorkOrderDetailDataService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public Task<bool> ExistsAsync(int workOrderId)
|
||||
=> WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking())
|
||||
.AnyAsync(w => w.Id == workOrderId);
|
||||
|
||||
public async Task<WorkOrderDetailExtendedFields?> GetExtendedFieldsAsync(int workOrderId)
|
||||
{
|
||||
return await WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking())
|
||||
.Where(w => w.Id == workOrderId)
|
||||
.Select(w => new WorkOrderDetailExtendedFields(
|
||||
w.Description,
|
||||
w.Trade,
|
||||
w.Problem,
|
||||
w.SubTrade,
|
||||
w.OriginalWeek,
|
||||
w.OriginalDate,
|
||||
w.CompletedDate,
|
||||
w.SignOffName,
|
||||
w.SignOffAttachment,
|
||||
w.SignOffSignature,
|
||||
w.ExtraServices,
|
||||
w.ServiceNotes,
|
||||
w.PrimaryDispatch != null && w.PrimaryDispatch.VendorNotes != null
|
||||
? w.PrimaryDispatch.VendorNotes
|
||||
: w.VendorNotes))
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Comments>> GetCommentsAsync(int workOrderId)
|
||||
{
|
||||
return await _context.Comments
|
||||
.AsNoTracking()
|
||||
.Where(c => c.WorkerOrderId == workOrderId && c.IsDeleted != true)
|
||||
.OrderByDescending(c => c.CreatedDate)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WorkOrderAuditLog>> GetAuditLogsAsync(int workOrderId, int? limit = null)
|
||||
{
|
||||
var query = _context.WorkOrderAuditLogs
|
||||
.AsNoTracking()
|
||||
.Where(a => a.WorkOrderId == workOrderId)
|
||||
.OrderByDescending(a => a.CreatedAt);
|
||||
|
||||
if (limit.HasValue)
|
||||
return await query.Take(limit.Value).ToListAsync();
|
||||
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WorkOrderAttachments>> GetAttachmentsAsync(int workOrderId)
|
||||
{
|
||||
return await _context.workOrderAttachments
|
||||
.AsNoTracking()
|
||||
.Where(a => a.WorkorderId == workOrderId && a.IsDeleted != true)
|
||||
.OrderByDescending(a => a.CreatedDate)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DispatchSignoffRow>> GetDispatchSignoffsAsync(int workOrderId)
|
||||
{
|
||||
return await _context.DispatchSignoffs
|
||||
.AsNoTracking()
|
||||
.Where(s => s.Dispatch != null && s.Dispatch.WorkOrderId == workOrderId && s.IsDeleted != true)
|
||||
.OrderByDescending(s => s.SignedAt)
|
||||
.Select(s => new DispatchSignoffRow(
|
||||
s.DispatchId,
|
||||
s.SignoffType,
|
||||
s.Name,
|
||||
s.SignedAt))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<WorkOrder?> GetWorkOrderForMediaAsync(int workOrderId)
|
||||
{
|
||||
return await WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking())
|
||||
.FirstOrDefaultAsync(w => w.Id == workOrderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
{
|
||||
public class WorkOrderDomainJobDataService : IWorkOrderDomainJobDataService
|
||||
{
|
||||
private static readonly LifecycleStatus?[] TerminalStatuses =
|
||||
{
|
||||
LifecycleStatus.Completed,
|
||||
LifecycleStatus.Canceled
|
||||
};
|
||||
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public WorkOrderDomainJobDataService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<int>> GetWeekRolledCandidatesAsync(
|
||||
DateOnly sourceWeekStart,
|
||||
DateOnly sourceWeekEnd,
|
||||
int batchSize = 500,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var weekStartDate = sourceWeekStart.ToDateTime(TimeOnly.MinValue);
|
||||
var weekEndDate = sourceWeekEnd.ToDateTime(TimeOnly.MinValue);
|
||||
|
||||
// Always return the first page of remaining unprocessed candidates.
|
||||
// Callers must not offset-page: ledger writes shrink this set after each batch.
|
||||
return await _context.workOrders
|
||||
.AsNoTracking()
|
||||
.Where(w => w.IsDeleted != true)
|
||||
.Where(w => w.istemplate != true)
|
||||
.Where(w => w.ScheduledDate != null)
|
||||
.Where(w =>
|
||||
w.ScheduledDate!.Value.Date >= weekStartDate.Date
|
||||
&& w.ScheduledDate.Value.Date <= weekEndDate.Date)
|
||||
.Where(w => !TerminalStatuses.Contains(w.LifecycleStatus))
|
||||
.Where(w => !_context.WorkOrderWeekRolledLedgers.Any(l =>
|
||||
l.WorkOrderId == w.Id && l.SourceWeekStart == sourceWeekStart))
|
||||
.OrderBy(w => w.Id)
|
||||
.Take(batchSize)
|
||||
.Select(w => w.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<WeekRolledProcessOutcome> TryProcessWeekRolledAsync(
|
||||
int workOrderId,
|
||||
DateOnly sourceWeekStart,
|
||||
string correlationId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
var alreadyProcessed = await _context.WorkOrderWeekRolledLedgers
|
||||
.AnyAsync(l => l.WorkOrderId == workOrderId && l.SourceWeekStart == sourceWeekStart, cancellationToken);
|
||||
|
||||
if (alreadyProcessed)
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
return new WeekRolledProcessOutcome { Skipped = true };
|
||||
}
|
||||
|
||||
var workOrder = await _context.workOrders
|
||||
.FirstOrDefaultAsync(w => w.Id == workOrderId, cancellationToken);
|
||||
|
||||
if (workOrder == null)
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
return new WeekRolledProcessOutcome { Skipped = true };
|
||||
}
|
||||
|
||||
_context.WorkOrderWeekRolledLedgers.Add(new WorkOrderWeekRolledLedger
|
||||
{
|
||||
WorkOrderId = workOrderId,
|
||||
SourceWeekStart = sourceWeekStart,
|
||||
ProcessedAt = DateTime.UtcNow,
|
||||
CorrelationId = correlationId
|
||||
});
|
||||
|
||||
var oldCarriedOver = workOrder.CarriedOver;
|
||||
workOrder.CarriedOver = oldCarriedOver + 1;
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
return new WeekRolledProcessOutcome { Skipped = true };
|
||||
}
|
||||
|
||||
return new WeekRolledProcessOutcome
|
||||
{
|
||||
Processed = true,
|
||||
OldCarriedOver = oldCarriedOver,
|
||||
NewCarriedOver = workOrder.CarriedOver
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<PastDueRefreshResult> RefreshPastDueFlagsAsync(
|
||||
DateTime utcNow,
|
||||
int batchSize = 500,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var today = utcNow.Date;
|
||||
var setCount = 0;
|
||||
var clearedCount = 0;
|
||||
var examinedCount = 0;
|
||||
var skip = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var batch = await _context.workOrders
|
||||
.Where(w => w.IsDeleted != true)
|
||||
.Where(w => w.istemplate != true)
|
||||
.Where(w => w.ScheduledDate != null)
|
||||
.OrderBy(w => w.Id)
|
||||
.Skip(skip)
|
||||
.Take(batchSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (batch.Count == 0)
|
||||
break;
|
||||
|
||||
foreach (var workOrder in batch)
|
||||
{
|
||||
examinedCount++;
|
||||
var shouldBePastDue = workOrder.ScheduledDate!.Value.Date < today
|
||||
&& !TerminalStatuses.Contains(workOrder.LifecycleStatus);
|
||||
var hasFlag = workOrder.OperationalFlags.HasFlag(OperationalFlags.PastDue);
|
||||
|
||||
if (shouldBePastDue && !hasFlag)
|
||||
{
|
||||
workOrder.OperationalFlags |= OperationalFlags.PastDue;
|
||||
setCount++;
|
||||
}
|
||||
else if (!shouldBePastDue && hasFlag)
|
||||
{
|
||||
workOrder.OperationalFlags &= ~OperationalFlags.PastDue;
|
||||
clearedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
skip += batchSize;
|
||||
|
||||
if (batch.Count < batchSize)
|
||||
break;
|
||||
}
|
||||
|
||||
return new PastDueRefreshResult
|
||||
{
|
||||
SetCount = setCount,
|
||||
ClearedCount = clearedCount,
|
||||
ExaminedCount = examinedCount
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ using Data.SeaHavenIndustries;
|
|||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public record SiteOptionRow(int Id, string? Code, string? City, string? State);
|
||||
|
||||
public interface ILocationDataService
|
||||
{
|
||||
Task<Locations?> GetByIdAsync(int id);
|
||||
|
|
@ -15,6 +17,8 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
// For addressbook endpoint - returns simplified DTO
|
||||
Task<(IEnumerable<object> Items, int TotalCount)> GetAddressbookPagedAsync(int page, int pageSize, string? search = null);
|
||||
|
||||
Task<IReadOnlyList<SiteOptionRow>> GetSiteOptionsAsync(string? search = null);
|
||||
|
||||
Task<Locations> AddAsync(Locations location);
|
||||
Task UpdateAsync(Locations location);
|
||||
Task DeleteAsync(int id);
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
Task<IEnumerable<ApplicationUser>> GetAllAsync();
|
||||
Task<ApplicationUser?> GetByEmailAsync(string email);
|
||||
Task<bool> ExistsAsync(string id);
|
||||
Task<IReadOnlyDictionary<string, string>> GetDisplayNamesByIdsAsync(IEnumerable<string> ids);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public interface IWorkOrderAdvancedSearchDataService
|
||||
{
|
||||
Task<WorkOrderAdvancedSearchResult> SearchAsync(WorkOrderAdvancedSearchQuery query);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public interface IWorkOrderBoardDataService
|
||||
{
|
||||
Task<WorkOrderBoardQueryResult> GetBoardRowsAsync(WorkOrderBoardQuery query);
|
||||
Task<IReadOnlyList<DispatcherLookupRow>> GetDispatcherLookupsAsync();
|
||||
Task<WorkOrderBoardRawRow?> GetBoardRowByIdAsync(int workOrderId);
|
||||
Task<bool> InternalWoNumberExistsAsync(string normalizedWoNumber, int excludeWorkOrderId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public interface IWorkOrderDetailDataService
|
||||
{
|
||||
Task<bool> ExistsAsync(int workOrderId);
|
||||
Task<WorkOrderDetailExtendedFields?> GetExtendedFieldsAsync(int workOrderId);
|
||||
Task<IReadOnlyList<Comments>> GetCommentsAsync(int workOrderId);
|
||||
Task<IReadOnlyList<WorkOrderAuditLog>> GetAuditLogsAsync(int workOrderId, int? limit = null);
|
||||
Task<IReadOnlyList<WorkOrderAttachments>> GetAttachmentsAsync(int workOrderId);
|
||||
Task<IReadOnlyList<DispatchSignoffRow>> GetDispatchSignoffsAsync(int workOrderId);
|
||||
Task<WorkOrder?> GetWorkOrderForMediaAsync(int workOrderId);
|
||||
}
|
||||
|
||||
public interface ICompletionDocTemplateDataService
|
||||
{
|
||||
Task<IReadOnlyList<CompletionDocTemplateRow>> GetActiveAsync(string? serviceKey, WorkOrderType? workOrderType);
|
||||
Task<CompletionDocTemplateRow?> GetByIdAsync(int id);
|
||||
Task<CompletionDocTemplateRow?> ResolveForWorkOrderAsync(string? trade, WorkOrderType? workOrderType);
|
||||
Task<CompletionDocTemplate> CreateAsync(CompletionDocTemplate template);
|
||||
Task UpdateAsync(CompletionDocTemplate template);
|
||||
Task<bool> DeleteAsync(int id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public sealed class WeekRolledProcessOutcome
|
||||
{
|
||||
public bool Processed { get; init; }
|
||||
public bool Skipped { get; init; }
|
||||
public int OldCarriedOver { get; init; }
|
||||
public int NewCarriedOver { get; init; }
|
||||
}
|
||||
|
||||
public sealed class PastDueRefreshResult
|
||||
{
|
||||
public int SetCount { get; init; }
|
||||
public int ClearedCount { get; init; }
|
||||
public int ExaminedCount { get; init; }
|
||||
}
|
||||
|
||||
public interface IWorkOrderDomainJobDataService
|
||||
{
|
||||
Task<IReadOnlyList<int>> GetWeekRolledCandidatesAsync(
|
||||
DateOnly sourceWeekStart,
|
||||
DateOnly sourceWeekEnd,
|
||||
int batchSize = 500,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<WeekRolledProcessOutcome> TryProcessWeekRolledAsync(
|
||||
int workOrderId,
|
||||
DateOnly sourceWeekStart,
|
||||
string correlationId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<PastDueRefreshResult> RefreshPastDueFlagsAsync(
|
||||
DateTime utcNow,
|
||||
int batchSize = 500,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public record PrimaryDispatchProjection(
|
||||
int WorkOrderId,
|
||||
int? DispatchId,
|
||||
string? VendorName,
|
||||
DateTime? ApptDate,
|
||||
string? DispatchStatus);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public record WorkOrderAdvancedSearchQuery(
|
||||
string? Search,
|
||||
DateOnly DateFrom,
|
||||
DateOnly DateTo,
|
||||
IReadOnlyList<string>? Sites,
|
||||
IReadOnlyList<WorkOrderType>? Types,
|
||||
IReadOnlyList<string>? Dispatchers,
|
||||
IReadOnlyList<LifecycleStatus>? Statuses,
|
||||
IReadOnlyList<string>? PmTypes,
|
||||
IReadOnlyList<int>? VendorIds,
|
||||
IReadOnlyList<DocStatus>? DocStatuses,
|
||||
bool MyWorkOrders,
|
||||
string? CurrentUserId,
|
||||
int Page,
|
||||
int PageSize,
|
||||
string SortBy,
|
||||
string SortDir);
|
||||
|
||||
public record WorkOrderAdvancedSearchResult(
|
||||
IReadOnlyList<WorkOrderBoardRawRow> Rows,
|
||||
int TotalCount);
|
||||
}
|
||||
65
SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs
Normal file
65
SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public record WorkOrderBoardQuery(
|
||||
DateOnly WeekStart,
|
||||
DateOnly WeekEnd,
|
||||
IReadOnlyList<string>? Dispatchers,
|
||||
bool MyWorkOrders,
|
||||
IReadOnlyList<WorkOrderType>? 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? Trade,
|
||||
string? Problem,
|
||||
string? ServiceNotes,
|
||||
string? ExtraServices,
|
||||
DocStatus? DocStatus,
|
||||
string? FlagColor,
|
||||
int? PrimaryDispatchId,
|
||||
byte[]? RowVersion,
|
||||
byte[]? DispatchRowVersion,
|
||||
bool IsUnscheduled);
|
||||
|
||||
public record WorkOrderBoardQueryResult(
|
||||
IReadOnlyList<WorkOrderBoardRawRow> ScheduledRows,
|
||||
IReadOnlyList<WorkOrderBoardRawRow> UnscheduledRows,
|
||||
int ScheduledTotalBeforeSearch);
|
||||
|
||||
public record DispatcherLookupRow(
|
||||
string Id,
|
||||
string FirstName,
|
||||
string? LastName,
|
||||
string? Initials,
|
||||
string? Color);
|
||||
}
|
||||
33
SeaHaven.DataServices/Interfaces/WorkOrderDetailModels.cs
Normal file
33
SeaHaven.DataServices/Interfaces/WorkOrderDetailModels.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public record WorkOrderDetailExtendedFields(
|
||||
string? Description,
|
||||
string? Trade,
|
||||
string? Problem,
|
||||
string? SubTrade,
|
||||
DateOnly? OriginalWeek,
|
||||
DateOnly? OriginalDate,
|
||||
DateTime? CompletedDate,
|
||||
string? SignOffName,
|
||||
string? SignOffAttachment,
|
||||
string? SignOffSignature,
|
||||
string? ExtraServices,
|
||||
string? ServiceNotes,
|
||||
string? VendorNotes);
|
||||
|
||||
public record DispatchSignoffRow(
|
||||
int DispatchId,
|
||||
string? SignoffType,
|
||||
string? Name,
|
||||
DateTime? SignedAt);
|
||||
|
||||
public record CompletionDocTemplateRow(
|
||||
int Id,
|
||||
string Name,
|
||||
string ServiceKey,
|
||||
WorkOrderType? WorkOrderType,
|
||||
string TemplateUrl,
|
||||
bool IsActive);
|
||||
}
|
||||
|
|
@ -8,8 +8,9 @@ public class PagedResultTests
|
|||
{
|
||||
[Theory]
|
||||
[InlineData(0, 10, 0)]
|
||||
[InlineData(1, 10, 1)]
|
||||
[InlineData(10, 10, 1)]
|
||||
[InlineData(25, 10, 3)]
|
||||
[InlineData(11, 10, 2)]
|
||||
[InlineData(100, 25, 4)]
|
||||
public void TotalPages_IsCeilingOfCountOverPageSize(int totalCount, int pageSize, int expectedPages)
|
||||
{
|
||||
|
|
@ -19,16 +20,32 @@ public class PagedResultTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void HasPrevious_IsFalseOnFirstPage_TrueAfterward()
|
||||
public void HasPrevious_IsFalseOnFirstPage_TrueAfterward_OneBased()
|
||||
{
|
||||
new PagedResult<string> { Page = 1, PageSize = 10, TotalCount = 30 }.HasPrevious.Should().BeFalse();
|
||||
new PagedResult<string> { Page = 2, PageSize = 10, TotalCount = 30 }.HasPrevious.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasNext_IsTrueBeforeLastPage_FalseOnLastPage()
|
||||
public void HasNext_IsTrueBeforeLastPage_FalseOnLastPage_OneBased()
|
||||
{
|
||||
new PagedResult<string> { Page = 2, PageSize = 10, TotalCount = 30 }.HasNext.Should().BeTrue();
|
||||
new PagedResult<string> { Page = 3, PageSize = 10, TotalCount = 30 }.HasNext.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasPrevious_And_HasNext_ZeroBased_WhenPageBaseIsZero()
|
||||
{
|
||||
var first = new PagedResult<string> { Page = 0, PageSize = 10, TotalCount = 30, PageBase = 0 };
|
||||
first.HasPrevious.Should().BeFalse();
|
||||
first.HasNext.Should().BeTrue();
|
||||
|
||||
var middle = new PagedResult<string> { Page = 1, PageSize = 10, TotalCount = 30, PageBase = 0 };
|
||||
middle.HasPrevious.Should().BeTrue();
|
||||
middle.HasNext.Should().BeTrue();
|
||||
|
||||
var last = new PagedResult<string> { Page = 2, PageSize = 10, TotalCount = 30, PageBase = 0 };
|
||||
last.HasPrevious.Should().BeTrue();
|
||||
last.HasNext.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
SeaHaven.Services/Board/WorkOrderBoardConstants.cs
Normal file
8
SeaHaven.Services/Board/WorkOrderBoardConstants.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace SeaHaven.Services.Board
|
||||
{
|
||||
public static class WorkOrderBoardConstants
|
||||
{
|
||||
public const int MaxWindowDays = 90;
|
||||
public const int ClientSideThreshold = 300;
|
||||
}
|
||||
}
|
||||
26
SeaHaven.Services/Constants/WorkOrderFieldNames.cs
Normal file
26
SeaHaven.Services/Constants/WorkOrderFieldNames.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
namespace SeaHaven.Services.Constants
|
||||
{
|
||||
public static class WorkOrderFieldNames
|
||||
{
|
||||
public const string LifecycleStatus = nameof(LifecycleStatus);
|
||||
public const string AssignTo = nameof(AssignTo);
|
||||
public const string ScheduledDate = nameof(ScheduledDate);
|
||||
public const string ScheduledEnd = nameof(ScheduledEnd);
|
||||
public const string TargetWeek = nameof(TargetWeek);
|
||||
public const string ScheduleWeekOnly = nameof(ScheduleWeekOnly);
|
||||
public const string DueDate = nameof(DueDate);
|
||||
public const string Description = nameof(Description);
|
||||
public const string WorkerOrderTitle = nameof(WorkerOrderTitle);
|
||||
public const string SiteCode = nameof(SiteCode);
|
||||
public const string Building = nameof(Building);
|
||||
public const string LocationId = nameof(LocationId);
|
||||
public const string WorkOrderType = nameof(WorkOrderType);
|
||||
public const string Service = nameof(Service);
|
||||
public const string DocStatus = nameof(DocStatus);
|
||||
public const string Trade = nameof(Trade);
|
||||
public const string Problem = nameof(Problem);
|
||||
public const string Priority = nameof(Priority);
|
||||
public const string InternalWONumber = nameof(InternalWONumber);
|
||||
public const string Status = nameof(Status);
|
||||
}
|
||||
}
|
||||
|
|
@ -48,8 +48,17 @@ namespace SeaHaven.Services.DTOs
|
|||
public int TotalCount { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
|
||||
public bool HasPrevious => Page > 1;
|
||||
public bool HasNext => Page < TotalPages;
|
||||
|
||||
/// <summary>
|
||||
/// Page numbering base for <see cref="HasPrevious"/> / <see cref="HasNext"/>.
|
||||
/// Default <c>1</c> (classic APIs). Work-order advanced search sets <c>0</c>.
|
||||
/// </summary>
|
||||
public int PageBase { get; set; } = 1;
|
||||
|
||||
public int TotalPages => PageSize <= 0 ? 0 : (int)Math.Ceiling(TotalCount / (double)PageSize);
|
||||
|
||||
public bool HasPrevious => PageBase <= 0 ? Page > 0 : Page > 1;
|
||||
|
||||
public bool HasNext => PageBase <= 0 ? Page + 1 < TotalPages : Page < TotalPages;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,4 +36,12 @@ namespace SeaHaven.Services.DTOs
|
|||
public string? Zipcode { get; set; }
|
||||
public int? AccountId { get; set; }
|
||||
}
|
||||
|
||||
public class SiteOptionDTO
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? Code { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? State { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
101
SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs
Normal file
101
SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.Helpers;
|
||||
|
||||
namespace SeaHaven.Services.DTOs
|
||||
{
|
||||
public class WorkOrderBoardQueryDto
|
||||
{
|
||||
public DateOnly WeekStart { get; set; }
|
||||
public DateOnly? WeekEnd { get; set; }
|
||||
public List<string>? Dispatchers { get; set; }
|
||||
public bool MyWorkOrders { get; set; }
|
||||
public List<WorkOrderType>? 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? Pm { get; set; }
|
||||
public string? ServiceNotes { get; set; }
|
||||
public List<string>? ExtraServices { get; set; }
|
||||
public DocStatus? DocStatus { get; set; }
|
||||
/// <summary>Board flag color (#RRGGBB). Null = no flag. Distinct from Color (dispatcher avatar).</summary>
|
||||
public string? FlagColor { get; set; }
|
||||
public int? PrimaryDispatchId { get; set; }
|
||||
public byte[]? RowVersion { get; set; }
|
||||
public byte[]? DispatchRowVersion { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
public string Field { get; set; } = "";
|
||||
public string? Value { get; set; }
|
||||
public string? WorkOrderVersion { get; set; }
|
||||
public string? DispatchVersion { get; set; }
|
||||
public int? PrimaryDispatchId { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderBoardConflictDto
|
||||
{
|
||||
public string Message { get; set; } = "The record was modified by another user. Refresh and retry.";
|
||||
public WorkOrderBoardRowDto? CurrentState { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderBoardValidationErrorDto
|
||||
{
|
||||
public string Code { get; set; } = "";
|
||||
public string Message { get; set; } = "";
|
||||
}
|
||||
|
||||
public class WorkOrderBoardResponseDto
|
||||
{
|
||||
public DateOnly WeekStart { get; set; }
|
||||
public DateOnly WeekEnd { get; set; }
|
||||
public WorkOrderBoardCountsDto Counts { get; set; } = new();
|
||||
public List<WorkOrderBoardRowDto> Unscheduled { get; set; } = new();
|
||||
public List<WorkOrderBoardRowDto> 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; }
|
||||
}
|
||||
}
|
||||
63
SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs
Normal file
63
SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.Helpers;
|
||||
|
||||
namespace SeaHaven.Services.DTOs
|
||||
{
|
||||
/// <summary>
|
||||
/// Payload for creating a board work order (SHOC create wizard).
|
||||
/// FE follow-up: extend WorkOrderBoardCreatePayload + mapper with PrimaryService,
|
||||
/// ExtraServices, ServiceNotes, PocName, PocPhone, VendorNotes, TechPhone; stop embedding
|
||||
/// POC/vendor/service notes in description (pocNotes already exists). Prefer structured
|
||||
/// fields; keep Trade+Description temporarily for legacy clients.
|
||||
/// </summary>
|
||||
public class WorkOrderBoardCreateRequestDto
|
||||
{
|
||||
public string? WoNumber { get; set; }
|
||||
public WorkOrderType WorkOrderType { get; set; }
|
||||
public string? SiteCode { get; set; }
|
||||
public string? Description { get; set; }
|
||||
/// <summary>Legacy alias for primary service; prefer <see cref="PrimaryService"/>.</summary>
|
||||
public string? Trade { get; set; }
|
||||
/// <summary>Primary service label; persisted as Trade / board pm.</summary>
|
||||
public string? PrimaryService { get; set; }
|
||||
public List<string>? ExtraServices { get; set; }
|
||||
public string? ServiceNotes { get; set; }
|
||||
public int? LocationId { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
public string? AssignTo { get; set; }
|
||||
public DateTime? ScheduledDate { get; set; }
|
||||
public DateOnly? TargetWeek { get; set; }
|
||||
public bool? ScheduleWeekOnly { get; set; }
|
||||
public DocStatus? DocStatus { get; set; }
|
||||
public int? VendorId { get; set; }
|
||||
public DateTime? ApptDate { get; set; }
|
||||
public string? ApptTime { get; set; }
|
||||
public int? PocContactId { get; set; }
|
||||
public string? PocName { get; set; }
|
||||
public string? PocPhone { get; set; }
|
||||
public string? PocNotes { get; set; }
|
||||
public string? TechPhone { get; set; }
|
||||
public string? VendorNotes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Advanced search / filter query for the board.</summary>
|
||||
public class WorkOrderAdvancedSearchQueryDto
|
||||
{
|
||||
public int Page { get; set; } = 0;
|
||||
public int PageSize { get; set; } = 100;
|
||||
public string? Search { get; set; }
|
||||
public WorkOrderAdvancedSearchDatePreset? DatePreset { get; set; }
|
||||
public DateOnly? DateFrom { get; set; }
|
||||
public DateOnly? DateTo { get; set; }
|
||||
public List<string>? Sites { get; set; }
|
||||
public List<WorkOrderType>? Types { get; set; }
|
||||
public List<string>? Dispatchers { get; set; }
|
||||
public List<LifecycleStatus>? Statuses { get; set; }
|
||||
public List<string>? PmTypes { get; set; }
|
||||
public List<int>? VendorIds { get; set; }
|
||||
public List<DocStatus>? DocStatuses { get; set; }
|
||||
public bool MyWorkOrders { get; set; }
|
||||
public string? SortBy { get; set; }
|
||||
public string? SortDir { get; set; }
|
||||
}
|
||||
}
|
||||
106
SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs
Normal file
106
SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.DTOs
|
||||
{
|
||||
public class WorkOrderDetailDto
|
||||
{
|
||||
public WorkOrderDetailInfoDto Info { get; set; } = new();
|
||||
public WorkOrderCompletionDto Completion { get; set; } = new();
|
||||
public List<WorkOrderCommentDto> Comments { get; set; } = new();
|
||||
public List<WorkOrderAuditEntryDto> Audit { get; set; } = new();
|
||||
public List<WorkOrderMediaFileDto> Media { get; set; } = new();
|
||||
}
|
||||
|
||||
public class WorkOrderDetailInfoDto : WorkOrderBoardRowDto
|
||||
{
|
||||
public string? Description { get; set; }
|
||||
public string? Trade { get; set; }
|
||||
public string? Problem { get; set; }
|
||||
public string? SubTrade { get; set; }
|
||||
public DateOnly? OriginalWeek { get; set; }
|
||||
public DateOnly? OriginalDate { get; set; }
|
||||
public DateTime? CompletedDate { get; set; }
|
||||
public string? VendorNotes { get; set; }
|
||||
}
|
||||
|
||||
public class CompletionDocTemplateDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string ServiceKey { get; set; } = "";
|
||||
public WorkOrderType? WorkOrderType { get; set; }
|
||||
public string TemplateUrl { get; set; } = "";
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class CompletionDocTemplateCreateDto
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string ServiceKey { get; set; } = "";
|
||||
public WorkOrderType? WorkOrderType { get; set; }
|
||||
public string TemplateUrl { get; set; } = "";
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
public class WorkOrderCompletionDto
|
||||
{
|
||||
public DocStatus? DocStatus { get; set; }
|
||||
public CompletionDocTemplateDto? Template { get; set; }
|
||||
public string? SignOffName { get; set; }
|
||||
public string? SignOffAttachment { get; set; }
|
||||
public string? SignOffSignature { get; set; }
|
||||
public List<DispatchSignoffDto> DispatchSignoffs { get; set; } = new();
|
||||
}
|
||||
|
||||
public class DispatchSignoffDto
|
||||
{
|
||||
public int DispatchId { get; set; }
|
||||
public string? SignoffType { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? SignedAt { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderCommentDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? AuthorId { get; set; }
|
||||
public string? AuthorName { get; set; }
|
||||
public string Text { get; set; } = "";
|
||||
public string Time { get; set; } = "";
|
||||
public string? Documents { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderCommentCreateDto
|
||||
{
|
||||
public string Text { get; set; } = "";
|
||||
}
|
||||
|
||||
public class WorkOrderAuditEntryDto
|
||||
{
|
||||
public string Type { get; set; } = "manual";
|
||||
public string? DispatcherId { get; set; }
|
||||
public string Action { get; set; } = "";
|
||||
public string? FieldName { get; set; }
|
||||
public string OldValue { get; set; } = "";
|
||||
public string NewValue { get; set; } = "";
|
||||
public string Time { get; set; } = "";
|
||||
public int? DispatchId { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderMediaFileDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public WorkOrderMediaCategory Category { get; set; }
|
||||
public string Url { get; set; } = "";
|
||||
public string? FileName { get; set; }
|
||||
public string? UploadedAt { get; set; }
|
||||
public bool IsLegacy { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderCompletionDocUploadDto
|
||||
{
|
||||
public string? SignOffName { get; set; }
|
||||
public string? SignOffSignature { get; set; }
|
||||
public string? WorkOrderVersion { get; set; }
|
||||
}
|
||||
}
|
||||
21
SeaHaven.Services/DTOs/WorkOrderJobDTOs.cs
Normal file
21
SeaHaven.Services/DTOs/WorkOrderJobDTOs.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
namespace SeaHaven.Services.DTOs
|
||||
{
|
||||
public sealed class WeekRolledJobResult
|
||||
{
|
||||
public int Processed { get; set; }
|
||||
public int Skipped { get; set; }
|
||||
public int Failed { get; set; }
|
||||
public string CorrelationId { get; set; } = "";
|
||||
public DateOnly SourceWeekStart { get; set; }
|
||||
public DateOnly SourceWeekEnd { get; set; }
|
||||
public long DurationMs { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PastDueCacheJobResult
|
||||
{
|
||||
public int SetCount { get; set; }
|
||||
public int ClearedCount { get; set; }
|
||||
public int ExaminedCount { get; set; }
|
||||
public long DurationMs { get; set; }
|
||||
}
|
||||
}
|
||||
54
SeaHaven.Services/DTOs/WorkOrderPhase7DTOs.cs
Normal file
54
SeaHaven.Services/DTOs/WorkOrderPhase7DTOs.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
namespace SeaHaven.Services.DTOs
|
||||
{
|
||||
public sealed class WorkOrderIngestPayloadDto
|
||||
{
|
||||
public required string ExternalWorkOrderId { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? WoStatus { get; set; }
|
||||
public string? Severity { get; set; }
|
||||
public string? Customer { get; set; }
|
||||
public string? SiteCode { get; set; }
|
||||
public string? Building { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
public DateTime? DateReported { get; set; }
|
||||
public DateTime? ScheduledStart { get; set; }
|
||||
public string? SourceEmailS3Key { get; set; }
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WorkOrderIngestBatchRequestDto
|
||||
{
|
||||
public List<WorkOrderIngestPayloadDto>? Items { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WorkOrderIngestItemResultDto
|
||||
{
|
||||
public required string ExternalWorkOrderId { get; set; }
|
||||
public int WorkOrderId { get; set; }
|
||||
public bool Created { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WorkOrderIngestBatchResultDto
|
||||
{
|
||||
public int Created { get; set; }
|
||||
public int Updated { get; set; }
|
||||
public int Skipped { get; set; }
|
||||
public List<WorkOrderIngestItemResultDto> Results { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class WorkOrderOpsHealthDto
|
||||
{
|
||||
public DateTime? LastWeekRolledRunUtc { get; set; }
|
||||
public DateTime? LastPastDueCacheRunUtc { get; set; }
|
||||
public string? LastWeekRolledError { get; set; }
|
||||
public string? LastPastDueCacheError { get; set; }
|
||||
public int SyncRejectedLast24h { get; set; }
|
||||
public int FieldLockCount { get; set; }
|
||||
public bool SyncEnabled { get; set; }
|
||||
public bool IngestEnabled { get; set; }
|
||||
public bool LegacyDeprecationEnabled { get; set; }
|
||||
public string? LegacySunsetDate { get; set; }
|
||||
public DateTime CheckedAtUtc { get; set; }
|
||||
}
|
||||
}
|
||||
25
SeaHaven.Services/Exceptions/WorkOrderBoardExceptions.cs
Normal file
25
SeaHaven.Services/Exceptions/WorkOrderBoardExceptions.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Exceptions
|
||||
{
|
||||
public class WorkOrderBoardValidationException : Exception
|
||||
{
|
||||
public WorkOrderBoardValidationException(string code, string message) : base(message)
|
||||
{
|
||||
Code = code;
|
||||
}
|
||||
|
||||
public string Code { get; }
|
||||
}
|
||||
|
||||
public class WorkOrderBoardConcurrencyException : Exception
|
||||
{
|
||||
public WorkOrderBoardConcurrencyException(WorkOrderBoardRowDto? currentState)
|
||||
: base("The record was modified by another user. Refresh and retry.")
|
||||
{
|
||||
CurrentState = currentState;
|
||||
}
|
||||
|
||||
public WorkOrderBoardRowDto? CurrentState { get; }
|
||||
}
|
||||
}
|
||||
85
SeaHaven.Services/Helpers/LifecycleStatusMapper.cs
Normal file
85
SeaHaven.Services/Helpers/LifecycleStatusMapper.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class LifecycleStatusMapper
|
||||
{
|
||||
public static LifecycleStatus? FromLegacyStatus(string? status)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(status)) return null;
|
||||
|
||||
return status.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"open" or "incomplete" => LifecycleStatus.Incomplete,
|
||||
"pending" or "onhold" or "on hold" => LifecycleStatus.Pending,
|
||||
"scheduled" => LifecycleStatus.Scheduled,
|
||||
"en route" or "enroute" => LifecycleStatus.EnRoute,
|
||||
"on site" or "onsite" => LifecycleStatus.OnSite,
|
||||
"inprogress" or "in progress" => LifecycleStatus.InProgress,
|
||||
"completed" or "complete" or "done" or "closed" => LifecycleStatus.Completed,
|
||||
"rescheduled" => LifecycleStatus.Rescheduled,
|
||||
"cancelled" or "canceled" => LifecycleStatus.Canceled,
|
||||
"pending quote" or "pendingquote" or "pendingapproval" or "pending approval" => LifecycleStatus.PendingQuote,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public static LifecycleStatus? ParseLifecycleStatus(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
|
||||
if (TryParseFeLabel(value, out var fromLabel))
|
||||
return fromLabel;
|
||||
|
||||
if (Enum.TryParse<LifecycleStatus>(value, true, out var parsed))
|
||||
return parsed;
|
||||
|
||||
return FromLegacyStatus(value);
|
||||
}
|
||||
|
||||
public static bool TryParseFeLabel(string? label, out LifecycleStatus status)
|
||||
{
|
||||
status = default;
|
||||
if (string.IsNullOrWhiteSpace(label)) return false;
|
||||
|
||||
return label.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"incomplete" => Set(LifecycleStatus.Incomplete, out status),
|
||||
"pending" => Set(LifecycleStatus.Pending, out status),
|
||||
"scheduled" => Set(LifecycleStatus.Scheduled, out status),
|
||||
"en route" => Set(LifecycleStatus.EnRoute, out status),
|
||||
"on site" => Set(LifecycleStatus.OnSite, out status),
|
||||
"in progress" => Set(LifecycleStatus.InProgress, out status),
|
||||
"completed" => Set(LifecycleStatus.Completed, out status),
|
||||
"rescheduled" => Set(LifecycleStatus.Rescheduled, out status),
|
||||
"canceled" => Set(LifecycleStatus.Canceled, out status),
|
||||
"pending quote" => Set(LifecycleStatus.PendingQuote, out status),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToFeLabel(LifecycleStatus status) => status switch
|
||||
{
|
||||
LifecycleStatus.Incomplete => "Incomplete",
|
||||
LifecycleStatus.Pending => "Pending",
|
||||
LifecycleStatus.Scheduled => "Scheduled",
|
||||
LifecycleStatus.EnRoute => "En Route",
|
||||
LifecycleStatus.OnSite => "On Site",
|
||||
LifecycleStatus.InProgress => "In Progress",
|
||||
LifecycleStatus.Completed => "Completed",
|
||||
LifecycleStatus.Rescheduled => "Rescheduled",
|
||||
LifecycleStatus.Canceled => "Canceled",
|
||||
LifecycleStatus.PendingQuote => "Pending Quote",
|
||||
_ => status.ToString()
|
||||
};
|
||||
|
||||
public static bool IsTerminal(LifecycleStatus? status)
|
||||
=> status is LifecycleStatus.Completed or LifecycleStatus.Canceled;
|
||||
|
||||
private static bool Set(LifecycleStatus value, out LifecycleStatus status)
|
||||
{
|
||||
status = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
87
SeaHaven.Services/Helpers/WorkOrderAuditProjection.cs
Normal file
87
SeaHaven.Services/Helpers/WorkOrderAuditProjection.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderAuditProjection
|
||||
{
|
||||
private const string UnassignedLabel = "Unassigned";
|
||||
|
||||
public static WorkOrderAuditEntryDto ToDto(
|
||||
WorkOrderAuditLog log,
|
||||
IReadOnlyDictionary<string, string>? assignToDisplayNames = null)
|
||||
{
|
||||
var oldValue = log.OldValue ?? "";
|
||||
var newValue = log.NewValue ?? "";
|
||||
|
||||
if (IsAssignToField(log.FieldName))
|
||||
{
|
||||
oldValue = ResolveAssignToDisplay(oldValue, assignToDisplayNames);
|
||||
newValue = ResolveAssignToDisplay(newValue, assignToDisplayNames);
|
||||
}
|
||||
|
||||
return new WorkOrderAuditEntryDto
|
||||
{
|
||||
Type = MapEventType(log.EventType),
|
||||
DispatcherId = log.UserId,
|
||||
Action = log.Action ?? "",
|
||||
FieldName = log.FieldName,
|
||||
OldValue = oldValue,
|
||||
NewValue = newValue,
|
||||
Time = log.CreatedAt.ToString("o"),
|
||||
DispatchId = log.DispatchId
|
||||
};
|
||||
}
|
||||
|
||||
public static List<WorkOrderAuditEntryDto> ToDtos(
|
||||
IEnumerable<WorkOrderAuditLog> logs,
|
||||
IReadOnlyDictionary<string, string>? assignToDisplayNames = null)
|
||||
=> logs.Select(log => ToDto(log, assignToDisplayNames)).ToList();
|
||||
|
||||
public static IEnumerable<string> CollectAssignToUserIds(IEnumerable<WorkOrderAuditLog> logs)
|
||||
{
|
||||
return logs
|
||||
.Where(l => IsAssignToField(l.FieldName))
|
||||
.SelectMany(l => new[] { l.OldValue, l.NewValue })
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v))
|
||||
.Select(v => v!)
|
||||
.Distinct(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
internal static bool IsAssignToField(string? fieldName)
|
||||
=> string.Equals(fieldName, "AssignTo", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(fieldName, "AssignedTo", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string ResolveAssignToDisplay(
|
||||
string value,
|
||||
IReadOnlyDictionary<string, string>? assignToDisplayNames)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return UnassignedLabel;
|
||||
|
||||
if (assignToDisplayNames != null
|
||||
&& assignToDisplayNames.TryGetValue(value, out var name)
|
||||
&& !string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string MapEventType(string? eventType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(eventType))
|
||||
return "manual";
|
||||
|
||||
return eventType.ToLowerInvariant() switch
|
||||
{
|
||||
"manual" => "manual",
|
||||
"system" => "system",
|
||||
"sync" => "sync",
|
||||
"vendor" => "vendor",
|
||||
_ => eventType.ToLowerInvariant()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
57
SeaHaven.Services/Helpers/WorkOrderBoardApptTimeParser.cs
Normal file
57
SeaHaven.Services/Helpers/WorkOrderBoardApptTimeParser.cs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
132
SeaHaven.Services/Helpers/WorkOrderBoardFieldMutations.cs
Normal file
132
SeaHaven.Services/Helpers/WorkOrderBoardFieldMutations.cs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.Exceptions;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderBoardFieldMutations
|
||||
{
|
||||
public static List<BoardFieldChange> ApplyAssignTo(WorkOrder workOrder, string? value, string auditField = "AssignTo")
|
||||
{
|
||||
var normalized = string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
var old = workOrder.AssignTo;
|
||||
var changes = new List<BoardFieldChange>();
|
||||
|
||||
if (old != normalized)
|
||||
{
|
||||
workOrder.AssignTo = normalized;
|
||||
changes.Add(BoardFieldChange.AssignmentChanged(auditField, old, normalized));
|
||||
}
|
||||
|
||||
changes.AddRange(ApplyAutoScheduleSideEffects(workOrder));
|
||||
return changes;
|
||||
}
|
||||
|
||||
public static List<BoardFieldChange> ApplyScheduledDate(
|
||||
WorkOrder workOrder,
|
||||
DateTime? parsed,
|
||||
string auditField = "ScheduledDate",
|
||||
bool incrementRescheduleOnChange = true)
|
||||
{
|
||||
var previousDate = workOrder.ScheduledDate;
|
||||
var changes = new List<BoardFieldChange>();
|
||||
var oldFormatted = FormatDate(previousDate);
|
||||
var newFormatted = FormatDate(parsed);
|
||||
|
||||
if (oldFormatted == newFormatted)
|
||||
return changes;
|
||||
|
||||
workOrder.ScheduledDate = parsed;
|
||||
changes.Add(BoardFieldChange.Changed(auditField, oldFormatted, newFormatted));
|
||||
|
||||
if (!workOrder.OriginalDate.HasValue && parsed.HasValue)
|
||||
{
|
||||
workOrder.OriginalDate = DateOnly.FromDateTime(parsed.Value);
|
||||
var originalWeek = WorkOrderBoardMutationRules.ComputeOriginalWeek(parsed);
|
||||
if (originalWeek.HasValue)
|
||||
workOrder.OriginalWeek = originalWeek;
|
||||
}
|
||||
|
||||
if (incrementRescheduleOnChange && WorkOrderBoardMutationRules.IsReschedule(previousDate, parsed))
|
||||
{
|
||||
var oldCount = workOrder.RescheduleCount.ToString();
|
||||
workOrder.RescheduleCount++;
|
||||
changes.Add(BoardFieldChange.Changed(
|
||||
WorkOrderBoardFieldNames.RescheduleCount,
|
||||
oldCount,
|
||||
workOrder.RescheduleCount.ToString()));
|
||||
}
|
||||
|
||||
changes.AddRange(ApplyAutoScheduleSideEffects(workOrder));
|
||||
return changes;
|
||||
}
|
||||
|
||||
public static List<BoardFieldChange> ApplyAutoScheduleSideEffects(WorkOrder workOrder)
|
||||
{
|
||||
var changes = new List<BoardFieldChange>();
|
||||
if (!WorkOrderBoardMutationRules.ShouldAutoSchedule(workOrder.LifecycleStatus, workOrder.ScheduledDate, workOrder.AssignTo))
|
||||
return changes;
|
||||
|
||||
var old = workOrder.LifecycleStatus?.ToString();
|
||||
workOrder.LifecycleStatus = LifecycleStatus.Scheduled;
|
||||
workOrder.Status = WorkOrderDerivedFields.GetLifecycleStatusLabel(LifecycleStatus.Scheduled);
|
||||
changes.Add(BoardFieldChange.StatusChanged("LifecycleStatus", old, LifecycleStatus.Scheduled.ToString()));
|
||||
return changes;
|
||||
}
|
||||
|
||||
public static BoardFieldChange ApplyApptTime(WorkOrder workOrder, Dispatch dispatch, string? value)
|
||||
{
|
||||
if (!WorkOrderBoardApptTimeParser.TryParse(value, out var start, out var end, out var error))
|
||||
throw new WorkOrderBoardValidationException("InvalidValue", error ?? "Invalid apptTime.");
|
||||
|
||||
var apptDate = dispatch.ScheduledDate ?? workOrder.ScheduledDate;
|
||||
var oldStart = FormatDateTime(workOrder.ScheduledStart);
|
||||
var oldEnd = FormatDateTime(workOrder.ScheduledEnd);
|
||||
|
||||
workOrder.ScheduledStart = WorkOrderBoardApptTimeParser.CombineDateAndTime(apptDate, start);
|
||||
workOrder.ScheduledEnd = WorkOrderBoardApptTimeParser.CombineDateAndTime(apptDate, end);
|
||||
|
||||
var newStart = FormatDateTime(workOrder.ScheduledStart);
|
||||
var newEnd = FormatDateTime(workOrder.ScheduledEnd);
|
||||
var oldCombined = FormatApptRange(oldStart, oldEnd);
|
||||
var newCombined = FormatApptRange(newStart, newEnd);
|
||||
|
||||
if (oldCombined == newCombined)
|
||||
return BoardFieldChange.Unchanged("ApptTime", dispatch.Id);
|
||||
|
||||
return BoardFieldChange.Changed("ApptTime", oldCombined, newCombined, dispatch.Id);
|
||||
}
|
||||
|
||||
public static void SetInitialLifecycleStatus(WorkOrder workOrder)
|
||||
{
|
||||
workOrder.LifecycleStatus = LifecycleStatus.Incomplete;
|
||||
workOrder.Status = WorkOrderDerivedFields.GetLifecycleStatusLabel(LifecycleStatus.Incomplete);
|
||||
}
|
||||
|
||||
private static string? FormatDate(DateTime? value) => value?.ToString("yyyy-MM-dd");
|
||||
private static string? FormatDateTime(DateTime? value) => value?.ToString("O");
|
||||
private static string? FormatApptRange(string? start, string? end)
|
||||
=> start == null && end == null ? null : $"{start}|{end}";
|
||||
|
||||
public sealed record BoardFieldChange(
|
||||
string FieldName,
|
||||
string? OldValue,
|
||||
string? NewValue,
|
||||
AuditActionType Action,
|
||||
int? DispatchId,
|
||||
bool HasChanged)
|
||||
{
|
||||
public static BoardFieldChange Unchanged(string fieldName, int? dispatchId = null)
|
||||
=> new(fieldName, null, null, AuditActionType.FieldChanged, dispatchId, false);
|
||||
|
||||
public static BoardFieldChange Changed(string fieldName, string? oldValue, string? newValue, int? dispatchId = null)
|
||||
=> new(fieldName, oldValue, newValue, AuditActionType.FieldChanged, dispatchId, true);
|
||||
|
||||
public static BoardFieldChange StatusChanged(string fieldName, string? oldValue, string? newValue)
|
||||
=> new(fieldName, oldValue, newValue, AuditActionType.StatusChanged, null, true);
|
||||
|
||||
public static BoardFieldChange AssignmentChanged(string fieldName, string? oldValue, string? newValue)
|
||||
=> new(fieldName, oldValue, newValue, AuditActionType.AssignmentChanged, null, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs
Normal file
94
SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderBoardFieldNames
|
||||
{
|
||||
public const string WoNumber = "woNumber";
|
||||
public const string WorkOrderType = "workOrderType";
|
||||
public const string SiteCode = "siteCode";
|
||||
public const string LifecycleStatus = "lifecycleStatus";
|
||||
public const string AssignTo = "assignTo";
|
||||
public const string DueDate = "dueDate";
|
||||
public const string ScheduledDate = "scheduledDate";
|
||||
public const string TargetWeek = "targetWeek";
|
||||
public const string ScheduleWeekOnly = "scheduleWeekOnly";
|
||||
public const string VendorId = "vendorId";
|
||||
public const string ApptDate = "apptDate";
|
||||
public const string ApptTime = "apptTime";
|
||||
public const string DocStatus = "docStatus";
|
||||
public const string Pm = "pm";
|
||||
public const string ServiceNotes = "serviceNotes";
|
||||
public const string ExtraServices = "extraServices";
|
||||
public const string FlagColor = "flagColor";
|
||||
public const string RescheduleCount = "RescheduleCount";
|
||||
|
||||
private static readonly HashSet<string> DispatchFields = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
VendorId,
|
||||
ApptDate,
|
||||
ApptTime
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> AllEditable = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
WoNumber,
|
||||
WorkOrderType,
|
||||
SiteCode,
|
||||
LifecycleStatus,
|
||||
AssignTo,
|
||||
DueDate,
|
||||
ScheduledDate,
|
||||
TargetWeek,
|
||||
ScheduleWeekOnly,
|
||||
VendorId,
|
||||
ApptDate,
|
||||
ApptTime,
|
||||
DocStatus,
|
||||
Pm,
|
||||
ServiceNotes,
|
||||
ExtraServices,
|
||||
FlagColor
|
||||
};
|
||||
|
||||
public static bool IsEditable(string field) => Canonicalize(field) != null;
|
||||
|
||||
public static bool IsDispatchField(string field)
|
||||
{
|
||||
var canonical = Canonicalize(field);
|
||||
return canonical != null && DispatchFields.Contains(canonical);
|
||||
}
|
||||
|
||||
public static string? Canonicalize(string field)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(field))
|
||||
return null;
|
||||
|
||||
return AllEditable.FirstOrDefault(f => string.Equals(f, field, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public static string ToAuditFieldName(string field)
|
||||
{
|
||||
var canonical = Canonicalize(field) ?? field;
|
||||
return canonical switch
|
||||
{
|
||||
WoNumber => "InternalWONumber",
|
||||
WorkOrderType => "WorkOrderType",
|
||||
SiteCode => "SiteCode",
|
||||
LifecycleStatus => "LifecycleStatus",
|
||||
AssignTo => "AssignTo",
|
||||
DueDate => "DueDate",
|
||||
ScheduledDate => "ScheduledDate",
|
||||
TargetWeek => "TargetWeek",
|
||||
ScheduleWeekOnly => "ScheduleWeekOnly",
|
||||
VendorId => "VendorId",
|
||||
ApptDate => "ApptDate",
|
||||
ApptTime => "ApptTime",
|
||||
DocStatus => "DocStatus",
|
||||
Pm => "Trade",
|
||||
ServiceNotes => "ServiceNotes",
|
||||
ExtraServices => "ExtraServices",
|
||||
FlagColor => "FlagColor",
|
||||
_ => canonical
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
42
SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs
Normal file
42
SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderBoardMutationRules
|
||||
{
|
||||
private static readonly HashSet<LifecycleStatus?> ReadOnlyStatuses = new()
|
||||
{
|
||||
LifecycleStatus.Canceled,
|
||||
LifecycleStatus.Completed
|
||||
};
|
||||
|
||||
public static bool IsReadOnly(LifecycleStatus? status) => ReadOnlyStatuses.Contains(status);
|
||||
|
||||
public static bool ShouldBlockStatusChangeWhenPastDue(string field, bool isPastDue)
|
||||
=> field.Equals(WorkOrderBoardFieldNames.LifecycleStatus, StringComparison.OrdinalIgnoreCase) && isPastDue;
|
||||
|
||||
public static bool ShouldAutoSchedule(
|
||||
LifecycleStatus? status,
|
||||
DateTime? scheduledDate,
|
||||
string? assignTo)
|
||||
=> status == LifecycleStatus.Incomplete
|
||||
&& scheduledDate.HasValue
|
||||
&& !string.IsNullOrWhiteSpace(assignTo);
|
||||
|
||||
public static bool IsReschedule(DateTime? previousDate, DateTime? newDate)
|
||||
=> previousDate.HasValue
|
||||
&& newDate.HasValue
|
||||
&& previousDate.Value.Date != newDate.Value.Date;
|
||||
|
||||
public static DateOnly? ComputeOriginalWeek(DateTime? scheduledDate)
|
||||
=> scheduledDate.HasValue
|
||||
? DateOnly.FromDateTime(GetWeekStartMonday(scheduledDate.Value))
|
||||
: null;
|
||||
|
||||
private static DateTime GetWeekStartMonday(DateTime date)
|
||||
{
|
||||
var diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
|
||||
return date.Date.AddDays(-diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
SeaHaven.Services/Helpers/WorkOrderBoardSortFields.cs
Normal file
29
SeaHaven.Services/Helpers/WorkOrderBoardSortFields.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderBoardSortFields
|
||||
{
|
||||
public static readonly IReadOnlySet<string> Allowed = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"scheduledDate",
|
||||
"woNumber",
|
||||
"dueDate"
|
||||
};
|
||||
|
||||
public static string NormalizeOrThrow(string? sortBy)
|
||||
{
|
||||
var field = (sortBy ?? "scheduledDate").Trim();
|
||||
if (!Allowed.Contains(field))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"sortBy must be one of: scheduledDate, woNumber, dueDate.");
|
||||
}
|
||||
|
||||
return field.ToLowerInvariant() switch
|
||||
{
|
||||
"wonumber" => "woNumber",
|
||||
"duedate" => "dueDate",
|
||||
_ => "scheduledDate"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
52
SeaHaven.Services/Helpers/WorkOrderCommentProjection.cs
Normal file
52
SeaHaven.Services/Helpers/WorkOrderCommentProjection.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderCommentProjection
|
||||
{
|
||||
public static WorkOrderCommentDto ToDto(
|
||||
Comments comment,
|
||||
IReadOnlyDictionary<string, string>? displayNames = null)
|
||||
{
|
||||
return new WorkOrderCommentDto
|
||||
{
|
||||
Id = comment.Id,
|
||||
AuthorId = comment.UserId,
|
||||
AuthorName = ResolveAuthorName(comment, displayNames),
|
||||
Text = comment.Commenttext ?? "",
|
||||
Time = (comment.CreatedDate ?? DateTime.UtcNow).ToUniversalTime().ToString("o"),
|
||||
Documents = comment.Documents
|
||||
};
|
||||
}
|
||||
|
||||
public static List<WorkOrderCommentDto> ToDtos(
|
||||
IEnumerable<Comments> comments,
|
||||
IReadOnlyDictionary<string, string>? displayNames = null)
|
||||
=> comments.Select(c => ToDto(c, displayNames)).ToList();
|
||||
|
||||
public static IEnumerable<string> CollectAuthorUserIds(IEnumerable<Comments> comments)
|
||||
=> comments
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.UserId))
|
||||
.Select(c => c.UserId!)
|
||||
.Distinct(StringComparer.Ordinal);
|
||||
|
||||
private static string? ResolveAuthorName(
|
||||
Comments comment,
|
||||
IReadOnlyDictionary<string, string>? displayNames)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(comment.UserId)
|
||||
&& displayNames != null
|
||||
&& displayNames.TryGetValue(comment.UserId, out var name)
|
||||
&& !string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(comment.Commenter))
|
||||
return comment.Commenter.Trim();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
99
SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs
Normal file
99
SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderDerivedFields
|
||||
{
|
||||
/// <summary>
|
||||
/// Past due overlay — never a WorkOrderType. Matches SHOC board design.
|
||||
/// </summary>
|
||||
public static bool IsPastDue(WorkOrder workOrder, DateOnly? asOf = null)
|
||||
{
|
||||
var today = asOf ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
if (workOrder.ScheduledDate == null)
|
||||
return false;
|
||||
|
||||
if (LifecycleStatusMapper.IsTerminal(workOrder.LifecycleStatus))
|
||||
return false;
|
||||
|
||||
return DateOnly.FromDateTime(workOrder.ScheduledDate.Value.Date) < today;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets initial lifecycle on create. New WOs start Incomplete unless auto-scheduled.
|
||||
/// </summary>
|
||||
public static void SetInitialLifecycleStatus(WorkOrder workOrder)
|
||||
{
|
||||
workOrder.LifecycleStatus ??= LifecycleStatus.Incomplete;
|
||||
ApplyAutoScheduleIfEligible(workOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHOC rule: specific scheduled date + assignee + Incomplete → Scheduled.
|
||||
/// Week-only targets do not auto-schedule.
|
||||
/// </summary>
|
||||
public static bool ApplyAutoScheduleIfEligible(WorkOrder workOrder)
|
||||
{
|
||||
if (workOrder.LifecycleStatus != LifecycleStatus.Incomplete)
|
||||
return false;
|
||||
|
||||
if (workOrder.ScheduleWeekOnly == true)
|
||||
return false;
|
||||
|
||||
if (workOrder.ScheduledDate == null)
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(workOrder.AssignTo))
|
||||
return false;
|
||||
|
||||
workOrder.LifecycleStatus = LifecycleStatus.Scheduled;
|
||||
return true;
|
||||
}
|
||||
|
||||
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) : "";
|
||||
}
|
||||
}
|
||||
50
SeaHaven.Services/Helpers/WorkOrderFlagColors.cs
Normal file
50
SeaHaven.Services/Helpers/WorkOrderFlagColors.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Fixed board flag palette (must match FE FLAG_COLORS). Values stored as canonical #RRGGBB.
|
||||
/// </summary>
|
||||
public static class WorkOrderFlagColors
|
||||
{
|
||||
public static readonly HashSet<string> Allowed = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"#EF4444",
|
||||
"#F97316",
|
||||
"#F59E0B",
|
||||
"#EAB308",
|
||||
"#22C55E",
|
||||
"#14B8A6",
|
||||
"#3B82F6",
|
||||
"#8B5CF6"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes to # + 6 uppercase hex digits, or null if empty.
|
||||
/// Returns false if the value is non-empty but not a valid hex color.
|
||||
/// </summary>
|
||||
public static bool TryNormalize(string? value, out string? canonical)
|
||||
{
|
||||
canonical = null;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return true;
|
||||
|
||||
var trimmed = value.Trim();
|
||||
if (trimmed.Length == 6 && trimmed[0] != '#')
|
||||
trimmed = "#" + trimmed;
|
||||
|
||||
if (trimmed.Length != 7 || trimmed[0] != '#')
|
||||
return false;
|
||||
|
||||
for (var i = 1; i < 7; i++)
|
||||
{
|
||||
if (!Char.IsAsciiHexDigit(trimmed[i]))
|
||||
return false;
|
||||
}
|
||||
|
||||
canonical = "#" + trimmed[1..].ToUpperInvariant();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsAllowed(string? canonical)
|
||||
=> canonical != null && Allowed.Contains(canonical);
|
||||
}
|
||||
}
|
||||
27
SeaHaven.Services/Helpers/WorkOrderIngestFieldMapper.cs
Normal file
27
SeaHaven.Services/Helpers/WorkOrderIngestFieldMapper.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
/// <summary>DynamoDB / ingest field mapping shared by Sync and direct ingest.</summary>
|
||||
public static class WorkOrderIngestFieldMapper
|
||||
{
|
||||
public static string? MapStatus(string? dynamoStatus)
|
||||
{
|
||||
return dynamoStatus switch
|
||||
{
|
||||
"new" => "Open",
|
||||
"assigned" => "Open",
|
||||
"in_progress" => "In Progress",
|
||||
"on_hold" => "On Hold",
|
||||
"completed" => "Done",
|
||||
"cancelled" => "Cancelled",
|
||||
"unknown" => "Open",
|
||||
_ => string.IsNullOrWhiteSpace(dynamoStatus) ? null : "Open"
|
||||
};
|
||||
}
|
||||
|
||||
public static string? MapSeverityToPriority(string? severity)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(severity)) return null;
|
||||
return $"Sev {severity}";
|
||||
}
|
||||
}
|
||||
}
|
||||
91
SeaHaven.Services/Helpers/WorkOrderMediaProjection.cs
Normal file
91
SeaHaven.Services/Helpers/WorkOrderMediaProjection.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderMediaProjection
|
||||
{
|
||||
public static List<WorkOrderMediaFileDto> ProjectAll(
|
||||
WorkOrder workOrder,
|
||||
IEnumerable<WorkOrderAttachments> attachments)
|
||||
{
|
||||
var media = new List<WorkOrderMediaFileDto>();
|
||||
var attachmentId = -1;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(workOrder.BeforPhotoAttachment))
|
||||
{
|
||||
media.Add(new WorkOrderMediaFileDto
|
||||
{
|
||||
Id = attachmentId--,
|
||||
Category = WorkOrderMediaCategory.Before,
|
||||
Url = workOrder.BeforPhotoAttachment,
|
||||
FileName = ExtractFileName(workOrder.BeforPhotoAttachment),
|
||||
IsLegacy = true
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(workOrder.AfterPhotoAttachment))
|
||||
{
|
||||
media.Add(new WorkOrderMediaFileDto
|
||||
{
|
||||
Id = attachmentId--,
|
||||
Category = WorkOrderMediaCategory.After,
|
||||
Url = workOrder.AfterPhotoAttachment,
|
||||
FileName = ExtractFileName(workOrder.AfterPhotoAttachment),
|
||||
IsLegacy = true
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(workOrder.SignOffAttachment))
|
||||
{
|
||||
media.Add(new WorkOrderMediaFileDto
|
||||
{
|
||||
Id = attachmentId--,
|
||||
Category = WorkOrderMediaCategory.Completion,
|
||||
Url = workOrder.SignOffAttachment,
|
||||
FileName = ExtractFileName(workOrder.SignOffAttachment),
|
||||
IsLegacy = true
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var attachment in attachments.Where(a => a.IsDeleted != true))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(attachment.Attachments))
|
||||
continue;
|
||||
|
||||
media.Add(new WorkOrderMediaFileDto
|
||||
{
|
||||
Id = attachment.Id,
|
||||
Category = attachment.Category ?? WorkOrderMediaCategory.Extra,
|
||||
Url = attachment.Attachments,
|
||||
FileName = ExtractFileName(attachment.Attachments),
|
||||
UploadedAt = attachment.CreatedDate?.ToUniversalTime().ToString("o"),
|
||||
IsLegacy = false
|
||||
});
|
||||
}
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
private static string? ExtractFileName(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return null;
|
||||
|
||||
var lastSlash = url.LastIndexOf('/');
|
||||
var segment = lastSlash >= 0 && lastSlash < url.Length - 1
|
||||
? url[(lastSlash + 1)..]
|
||||
: url;
|
||||
|
||||
try
|
||||
{
|
||||
return Uri.UnescapeDataString(segment);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return segment;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
SeaHaven.Services/Helpers/WorkOrderNumberNormalizer.cs
Normal file
37
SeaHaven.Services/Helpers/WorkOrderNumberNormalizer.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderNumberNormalizer
|
||||
{
|
||||
private const int RequiredLength = 11;
|
||||
|
||||
public static bool TryNormalize(string? input, out string normalized, out string? error)
|
||||
{
|
||||
normalized = "";
|
||||
error = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
error = "WO number is required.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var digits = Regex.Replace(input.Trim(), @"\D", "");
|
||||
if (digits.Length == 0)
|
||||
{
|
||||
error = "WO number must contain digits.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (digits.Length > RequiredLength)
|
||||
{
|
||||
error = $"WO number cannot exceed {RequiredLength} digits.";
|
||||
return false;
|
||||
}
|
||||
|
||||
normalized = digits.PadLeft(RequiredLength, '0');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
SeaHaven.Services/Helpers/WorkOrderOperationalWeek.cs
Normal file
20
SeaHaven.Services/Helpers/WorkOrderOperationalWeek.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
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}";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public enum WorkOrderAdvancedSearchDatePreset
|
||||
{
|
||||
ThisWeek,
|
||||
LastWeek,
|
||||
ThisMonth,
|
||||
Last3Months,
|
||||
NextWeek,
|
||||
NextMonth,
|
||||
Custom
|
||||
}
|
||||
|
||||
public record WorkOrderSearchDateRange(DateOnly From, DateOnly To);
|
||||
|
||||
public static class WorkOrderSearchDateRangeResolver
|
||||
{
|
||||
public static WorkOrderSearchDateRange Resolve(
|
||||
WorkOrderAdvancedSearchDatePreset? preset,
|
||||
DateOnly? dateFrom,
|
||||
DateOnly? dateTo,
|
||||
DateOnly? referenceDate = null)
|
||||
{
|
||||
var today = referenceDate ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var resolvedPreset = preset ?? WorkOrderAdvancedSearchDatePreset.ThisWeek;
|
||||
|
||||
if (resolvedPreset == WorkOrderAdvancedSearchDatePreset.Custom)
|
||||
{
|
||||
if (!dateFrom.HasValue || !dateTo.HasValue)
|
||||
throw new ArgumentException("dateFrom and dateTo are required when datePreset is custom.");
|
||||
|
||||
if (dateTo.Value < dateFrom.Value)
|
||||
throw new ArgumentException("dateTo must be on or after dateFrom.");
|
||||
|
||||
return new WorkOrderSearchDateRange(dateFrom.Value, dateTo.Value);
|
||||
}
|
||||
|
||||
return resolvedPreset switch
|
||||
{
|
||||
WorkOrderAdvancedSearchDatePreset.ThisWeek => ThisIsoWeek(today),
|
||||
WorkOrderAdvancedSearchDatePreset.LastWeek => LastIsoWeek(today),
|
||||
WorkOrderAdvancedSearchDatePreset.ThisMonth => ThisMonth(today),
|
||||
WorkOrderAdvancedSearchDatePreset.Last3Months => Last3Months(today),
|
||||
WorkOrderAdvancedSearchDatePreset.NextWeek => NextIsoWeek(today),
|
||||
WorkOrderAdvancedSearchDatePreset.NextMonth => NextMonth(today),
|
||||
_ => ThisIsoWeek(today)
|
||||
};
|
||||
}
|
||||
|
||||
public static DateOnly GetIsoWeekStart(DateOnly date)
|
||||
{
|
||||
var diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
|
||||
return date.AddDays(-diff);
|
||||
}
|
||||
|
||||
private static WorkOrderSearchDateRange ThisIsoWeek(DateOnly today)
|
||||
{
|
||||
var start = GetIsoWeekStart(today);
|
||||
return new WorkOrderSearchDateRange(start, start.AddDays(4));
|
||||
}
|
||||
|
||||
private static WorkOrderSearchDateRange LastIsoWeek(DateOnly today)
|
||||
{
|
||||
var start = GetIsoWeekStart(today).AddDays(-7);
|
||||
return new WorkOrderSearchDateRange(start, start.AddDays(4));
|
||||
}
|
||||
|
||||
private static WorkOrderSearchDateRange NextIsoWeek(DateOnly today)
|
||||
{
|
||||
var start = GetIsoWeekStart(today).AddDays(7);
|
||||
return new WorkOrderSearchDateRange(start, start.AddDays(4));
|
||||
}
|
||||
|
||||
private static WorkOrderSearchDateRange ThisMonth(DateOnly today)
|
||||
{
|
||||
var start = new DateOnly(today.Year, today.Month, 1);
|
||||
var end = start.AddMonths(1).AddDays(-1);
|
||||
return new WorkOrderSearchDateRange(start, end);
|
||||
}
|
||||
|
||||
private static WorkOrderSearchDateRange NextMonth(DateOnly today)
|
||||
{
|
||||
var start = new DateOnly(today.Year, today.Month, 1).AddMonths(1);
|
||||
var end = start.AddMonths(1).AddDays(-1);
|
||||
return new WorkOrderSearchDateRange(start, end);
|
||||
}
|
||||
|
||||
private static WorkOrderSearchDateRange Last3Months(DateOnly today)
|
||||
{
|
||||
var end = today;
|
||||
var start = today.AddMonths(-3);
|
||||
return new WorkOrderSearchDateRange(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
SeaHaven.Services/Helpers/WorkOrderTypeMapper.cs
Normal file
47
SeaHaven.Services/Helpers/WorkOrderTypeMapper.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderTypeMapper
|
||||
{
|
||||
/// <summary>Board FE types. Overdue is not a type — use <see cref="WorkOrderDerivedFields.IsPastDue(System.DateTime?, LifecycleStatus?, System.DateTime?)"/>.</summary>
|
||||
public static bool TryParseFeLabel(string? label, out WorkOrderType type)
|
||||
{
|
||||
type = default;
|
||||
if (string.IsNullOrWhiteSpace(label)) return false;
|
||||
|
||||
return label.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"pm" => Set(WorkOrderType.PM, out type),
|
||||
"reactive" => Set(WorkOrderType.Reactive, out type),
|
||||
"emergency" => Set(WorkOrderType.Emergency, out type),
|
||||
"add-on" or "addon" => Set(WorkOrderType.AddOn, out type),
|
||||
"po" => Set(WorkOrderType.PO, out type),
|
||||
"project" => Set(WorkOrderType.Project, out type),
|
||||
"inspection" => Set(WorkOrderType.Inspection, out type),
|
||||
"other" => Set(WorkOrderType.Other, out type),
|
||||
"overdue" => false,
|
||||
_ => Enum.TryParse(label, true, out type)
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToFeLabel(WorkOrderType type) => type switch
|
||||
{
|
||||
WorkOrderType.PM => "PM",
|
||||
WorkOrderType.Reactive => "Reactive",
|
||||
WorkOrderType.Emergency => "Emergency",
|
||||
WorkOrderType.AddOn => "Add-On",
|
||||
WorkOrderType.PO => "PO",
|
||||
WorkOrderType.Project => "Project",
|
||||
WorkOrderType.Inspection => "Inspection",
|
||||
WorkOrderType.Other => "Other",
|
||||
_ => type.ToString()
|
||||
};
|
||||
|
||||
private static bool Set(WorkOrderType value, out WorkOrderType type)
|
||||
{
|
||||
type = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -64,6 +64,20 @@ namespace SeaHaven.Services.Implementation
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SiteOptionDTO>> GetSiteOptionsAsync(string? search = null)
|
||||
{
|
||||
var rows = await _locationDataService.GetSiteOptionsAsync(search);
|
||||
return rows
|
||||
.Select(r => new SiteOptionDTO
|
||||
{
|
||||
Id = r.Id,
|
||||
Code = r.Code,
|
||||
City = r.City,
|
||||
State = r.State
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<LocationDTO> CreateLocationAsync(CreateLocationDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
|
|
|
|||
48
SeaHaven.Services/Implementation/PastDueCacheService.cs
Normal file
48
SeaHaven.Services/Implementation/PastDueCacheService.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
public class PastDueCacheService : IPastDueCacheService
|
||||
{
|
||||
private readonly IWorkOrderDomainJobDataService _dataService;
|
||||
private readonly ILogger<PastDueCacheService> _logger;
|
||||
|
||||
public PastDueCacheService(
|
||||
IWorkOrderDomainJobDataService dataService,
|
||||
ILogger<PastDueCacheService> logger)
|
||||
{
|
||||
_dataService = dataService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<PastDueCacheJobResult> RefreshAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
_logger.LogInformation("PastDue cache refresh started at {UtcNow}", utcNow);
|
||||
|
||||
var refreshResult = await _dataService.RefreshPastDueFlagsAsync(utcNow, cancellationToken: cancellationToken);
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
var result = new PastDueCacheJobResult
|
||||
{
|
||||
SetCount = refreshResult.SetCount,
|
||||
ClearedCount = refreshResult.ClearedCount,
|
||||
ExaminedCount = refreshResult.ExaminedCount,
|
||||
DurationMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"PastDue cache refresh completed. Set={SetCount}, Cleared={ClearedCount}, Examined={ExaminedCount}, DurationMs={DurationMs}",
|
||||
result.SetCount, result.ClearedCount, result.ExaminedCount, result.DurationMs);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
219
SeaHaven.Services/Implementation/SyncFieldMergePolicy.cs
Normal file
219
SeaHaven.Services/Implementation/SyncFieldMergePolicy.cs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.Constants;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
public class SyncFieldMergePolicy : ISyncFieldMergePolicy
|
||||
{
|
||||
private static readonly HashSet<string> ShocOwnedFieldSet = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
WorkOrderFieldNames.LifecycleStatus,
|
||||
WorkOrderFieldNames.AssignTo,
|
||||
WorkOrderFieldNames.ScheduledDate,
|
||||
WorkOrderFieldNames.ScheduledEnd,
|
||||
WorkOrderFieldNames.TargetWeek,
|
||||
WorkOrderFieldNames.ScheduleWeekOnly,
|
||||
WorkOrderFieldNames.DueDate,
|
||||
WorkOrderFieldNames.Description,
|
||||
WorkOrderFieldNames.WorkerOrderTitle,
|
||||
WorkOrderFieldNames.SiteCode,
|
||||
WorkOrderFieldNames.Building,
|
||||
WorkOrderFieldNames.LocationId,
|
||||
WorkOrderFieldNames.WorkOrderType,
|
||||
WorkOrderFieldNames.Service,
|
||||
WorkOrderFieldNames.DocStatus,
|
||||
WorkOrderFieldNames.Trade,
|
||||
WorkOrderFieldNames.Problem,
|
||||
WorkOrderFieldNames.Priority,
|
||||
WorkOrderFieldNames.InternalWONumber,
|
||||
WorkOrderFieldNames.Status,
|
||||
};
|
||||
|
||||
private readonly IWorkOrderFieldLockService _fieldLocks;
|
||||
|
||||
public SyncFieldMergePolicy(IWorkOrderFieldLockService fieldLocks)
|
||||
{
|
||||
_fieldLocks = fieldLocks;
|
||||
}
|
||||
|
||||
public IReadOnlySet<string> ShocOwnedFields => ShocOwnedFieldSet;
|
||||
|
||||
public async Task<bool> CanSyncOverwriteAsync(int workOrderId, string fieldName)
|
||||
=> !await _fieldLocks.IsLockedAsync(workOrderId, fieldName);
|
||||
|
||||
public async Task<bool> TryApplyAsync(WorkOrderSyncContext context, string fieldName, object? incomingValue)
|
||||
{
|
||||
if (incomingValue == null) return false;
|
||||
|
||||
if (await context.FieldLocks.IsLockedAsync(context.WorkOrder.Id, fieldName))
|
||||
{
|
||||
var current = GetFieldValue(context.WorkOrder, fieldName);
|
||||
await context.Audit.LogSyncRejectedAsync(
|
||||
context.WorkOrder.Id,
|
||||
fieldName,
|
||||
current,
|
||||
incomingValue.ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryApplyField(context.WorkOrder, fieldName, incomingValue);
|
||||
}
|
||||
|
||||
private static string? GetFieldValue(WorkOrder wo, string fieldName) => fieldName switch
|
||||
{
|
||||
WorkOrderFieldNames.Description => wo.Description,
|
||||
WorkOrderFieldNames.WorkerOrderTitle => wo.WorkerOrderTitle,
|
||||
WorkOrderFieldNames.Status => wo.Status,
|
||||
WorkOrderFieldNames.DueDate => wo.DueDate?.ToString("O"),
|
||||
WorkOrderFieldNames.ScheduledDate => wo.ScheduledDate?.ToString("O"),
|
||||
WorkOrderFieldNames.ScheduledEnd => wo.ScheduledEnd?.ToString("O"),
|
||||
WorkOrderFieldNames.TargetWeek => wo.TargetWeek?.ToString("O"),
|
||||
WorkOrderFieldNames.ScheduleWeekOnly => wo.ScheduleWeekOnly?.ToString(),
|
||||
WorkOrderFieldNames.SiteCode => wo.SiteCode,
|
||||
WorkOrderFieldNames.Building => wo.Building,
|
||||
WorkOrderFieldNames.Priority => wo.Priority,
|
||||
WorkOrderFieldNames.AssignTo => wo.AssignTo,
|
||||
WorkOrderFieldNames.LocationId => wo.LocationId?.ToString(),
|
||||
WorkOrderFieldNames.LifecycleStatus => wo.LifecycleStatus?.ToString(),
|
||||
WorkOrderFieldNames.WorkOrderType => wo.WorkOrderType?.ToString(),
|
||||
WorkOrderFieldNames.Service => wo.Service,
|
||||
WorkOrderFieldNames.DocStatus => wo.DocStatus?.ToString(),
|
||||
WorkOrderFieldNames.Trade => wo.Trade,
|
||||
WorkOrderFieldNames.Problem => wo.Problem,
|
||||
WorkOrderFieldNames.InternalWONumber => wo.InternalWONumber,
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static bool TryApplyField(WorkOrder wo, string fieldName, object incomingValue)
|
||||
{
|
||||
switch (fieldName)
|
||||
{
|
||||
case WorkOrderFieldNames.Description:
|
||||
wo.Description = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.WorkerOrderTitle:
|
||||
wo.WorkerOrderTitle = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.Status:
|
||||
wo.Status = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.DueDate when TryParseDateTime(incomingValue, out var dueDate):
|
||||
wo.DueDate = dueDate;
|
||||
return true;
|
||||
case WorkOrderFieldNames.ScheduledDate when TryParseDateTime(incomingValue, out var scheduledDate):
|
||||
wo.ScheduledDate = scheduledDate;
|
||||
return true;
|
||||
case WorkOrderFieldNames.ScheduledEnd when TryParseDateTime(incomingValue, out var scheduledEnd):
|
||||
wo.ScheduledEnd = scheduledEnd;
|
||||
return true;
|
||||
case WorkOrderFieldNames.TargetWeek when TryParseDateOnly(incomingValue, out var targetWeek):
|
||||
wo.TargetWeek = targetWeek;
|
||||
return true;
|
||||
case WorkOrderFieldNames.ScheduleWeekOnly when TryParseBool(incomingValue, out var scheduleWeekOnly):
|
||||
wo.ScheduleWeekOnly = scheduleWeekOnly;
|
||||
return true;
|
||||
case WorkOrderFieldNames.SiteCode:
|
||||
wo.SiteCode = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.Building:
|
||||
wo.Building = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.Priority:
|
||||
wo.Priority = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.AssignTo:
|
||||
wo.AssignTo = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.LocationId when TryParseInt(incomingValue, out var locationId):
|
||||
wo.LocationId = locationId;
|
||||
return true;
|
||||
case WorkOrderFieldNames.LifecycleStatus when TryParseEnum(incomingValue, out LifecycleStatus lifecycleStatus):
|
||||
wo.LifecycleStatus = lifecycleStatus;
|
||||
return true;
|
||||
case WorkOrderFieldNames.WorkOrderType when TryParseEnum(incomingValue, out WorkOrderType workOrderType):
|
||||
wo.WorkOrderType = workOrderType;
|
||||
return true;
|
||||
case WorkOrderFieldNames.Service:
|
||||
wo.Service = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.DocStatus when TryParseEnum(incomingValue, out DocStatus docStatus):
|
||||
wo.DocStatus = docStatus;
|
||||
return true;
|
||||
case WorkOrderFieldNames.Trade:
|
||||
wo.Trade = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.Problem:
|
||||
wo.Problem = incomingValue.ToString();
|
||||
return true;
|
||||
case WorkOrderFieldNames.InternalWONumber:
|
||||
wo.InternalWONumber = incomingValue.ToString();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseDateTime(object value, out DateTime result)
|
||||
{
|
||||
if (value is DateTime dt)
|
||||
{
|
||||
result = dt;
|
||||
return true;
|
||||
}
|
||||
|
||||
return DateTime.TryParse(value.ToString(), out result);
|
||||
}
|
||||
|
||||
private static bool TryParseDateOnly(object value, out DateOnly result)
|
||||
{
|
||||
if (value is DateOnly dateOnly)
|
||||
{
|
||||
result = dateOnly;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is DateTime dt)
|
||||
{
|
||||
result = DateOnly.FromDateTime(dt);
|
||||
return true;
|
||||
}
|
||||
|
||||
return DateOnly.TryParse(value.ToString(), out result);
|
||||
}
|
||||
|
||||
private static bool TryParseBool(object value, out bool result)
|
||||
{
|
||||
if (value is bool b)
|
||||
{
|
||||
result = b;
|
||||
return true;
|
||||
}
|
||||
|
||||
return bool.TryParse(value.ToString(), out result);
|
||||
}
|
||||
|
||||
private static bool TryParseInt(object value, out int result)
|
||||
{
|
||||
if (value is int i)
|
||||
{
|
||||
result = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
return int.TryParse(value.ToString(), out result);
|
||||
}
|
||||
|
||||
private static bool TryParseEnum<TEnum>(object value, out TEnum result) where TEnum : struct, Enum
|
||||
{
|
||||
if (value is TEnum enumValue)
|
||||
{
|
||||
result = enumValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
return Enum.TryParse(value.ToString(), true, out result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
using SeaHaven.DataServices.Helpers;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Helpers;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
public class WorkOrderAdvancedSearchService : IWorkOrderAdvancedSearchService
|
||||
{
|
||||
private const int DefaultPageSize = 100;
|
||||
private const int MaxPageSize = 100;
|
||||
|
||||
private readonly IWorkOrderAdvancedSearchDataService _searchDataService;
|
||||
|
||||
public WorkOrderAdvancedSearchService(IWorkOrderAdvancedSearchDataService searchDataService)
|
||||
{
|
||||
_searchDataService = searchDataService;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<WorkOrderBoardRowDto>> SearchAsync(
|
||||
WorkOrderAdvancedSearchQueryDto query,
|
||||
string? currentUserId)
|
||||
{
|
||||
var page = query.Page < 0 ? 0 : query.Page;
|
||||
var pageSize = query.PageSize < 1 ? DefaultPageSize : Math.Min(query.PageSize, MaxPageSize);
|
||||
|
||||
var dateRange = WorkOrderSearchDateRangeResolver.Resolve(
|
||||
query.DatePreset,
|
||||
query.DateFrom,
|
||||
query.DateTo);
|
||||
|
||||
var sortBy = WorkOrderBoardSortFields.NormalizeOrThrow(query.SortBy);
|
||||
|
||||
var dataQuery = new WorkOrderAdvancedSearchQuery(
|
||||
WorkOrderBoardSearchFilter.NormalizeSearch(query.Search),
|
||||
dateRange.From,
|
||||
dateRange.To,
|
||||
query.Sites,
|
||||
query.Types,
|
||||
query.Dispatchers,
|
||||
query.Statuses,
|
||||
query.PmTypes,
|
||||
query.VendorIds,
|
||||
query.DocStatuses,
|
||||
query.MyWorkOrders,
|
||||
currentUserId,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
query.SortDir ?? "asc");
|
||||
|
||||
var result = await _searchDataService.SearchAsync(dataQuery);
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
return new PagedResult<WorkOrderBoardRowDto>
|
||||
{
|
||||
Items = result.Rows.Select(r => WorkOrderBoardService.MapRawRow(r, utcNow)).ToList(),
|
||||
TotalCount = result.TotalCount,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
PageBase = 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue