mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 16:19:07 +00:00
refactor(workorders): enforce service and data boundaries
This commit is contained in:
parent
9cd1c7e7fc
commit
848e973d62
39 changed files with 3535 additions and 1675 deletions
280
Api.SeaHavenIndustries.Tests/ServiceBehaviorTests.cs
Normal file
280
Api.SeaHavenIndustries.Tests/ServiceBehaviorTests.cs
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Moq;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Implementation;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
using SeaHaven.Services.Validation;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Tests;
|
||||||
|
|
||||||
|
internal static class TestAsyncEnumerableHelper
|
||||||
|
{
|
||||||
|
public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(this IEnumerable<T> source)
|
||||||
|
{
|
||||||
|
foreach (var item in source)
|
||||||
|
yield return item;
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SyncServiceBehaviorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task SyncWorkOrders_CreatesNewAndUpdatesExisting()
|
||||||
|
{
|
||||||
|
var data = new Mock<ISyncDataService>();
|
||||||
|
var external = new Mock<ISyncExternalSource>();
|
||||||
|
|
||||||
|
var existingWO = new WorkOrder { Id = 5, ExternalWorkOrderId = "EXT-1", WorkerOrderTitle = "Old" };
|
||||||
|
|
||||||
|
data.Setup(d => d.GetLastInternalWONumberAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync("10000005");
|
||||||
|
data.Setup(d => d.GetWorkOrderByExternalIdAsync("EXT-1", It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(existingWO);
|
||||||
|
data.Setup(d => d.GetWorkOrderByExternalIdAsync("EXT-2", It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync((WorkOrder?)null);
|
||||||
|
data.Setup(d => d.ResolveLocationAsync(It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync((int?)null);
|
||||||
|
data.Setup(d => d.GetLocationCountAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(3);
|
||||||
|
|
||||||
|
var items = new List<Dictionary<string, string?>>
|
||||||
|
{
|
||||||
|
new() { ["work_order_id"] = "EXT-1", ["description"] = "Updated Title", ["wo_status"] = "in_progress" },
|
||||||
|
new() { ["work_order_id"] = "EXT-2", ["description"] = "New WO", ["wo_status"] = "new", ["severity"] = "3" }
|
||||||
|
};
|
||||||
|
|
||||||
|
external.Setup(e => e.ScanWorkOrdersAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(items.ToAsyncEnumerable());
|
||||||
|
|
||||||
|
var service = new SyncService(data.Object, external.Object);
|
||||||
|
var result = await service.SyncWorkOrdersAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Synced);
|
||||||
|
Assert.Equal(1, result.Created);
|
||||||
|
Assert.Equal(1, result.Updated);
|
||||||
|
Assert.Equal(3, result.LocationsCreated);
|
||||||
|
Assert.Equal("Updated Title", existingWO.WorkerOrderTitle);
|
||||||
|
Assert.Equal("In Progress", existingWO.Status);
|
||||||
|
data.Verify(d => d.EnqueueWorkOrder(It.Is<WorkOrder>(w => w.ExternalWorkOrderId == "EXT-2")), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SyncWorkOrders_MapsStatusAndSeverity()
|
||||||
|
{
|
||||||
|
var data = new Mock<ISyncDataService>();
|
||||||
|
var external = new Mock<ISyncExternalSource>();
|
||||||
|
|
||||||
|
data.Setup(d => d.GetLastInternalWONumberAsync(It.IsAny<CancellationToken>())).ReturnsAsync((string?)null);
|
||||||
|
data.Setup(d => d.GetWorkOrderByExternalIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync((WorkOrder?)null);
|
||||||
|
data.Setup(d => d.ResolveLocationAsync(It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>())).ReturnsAsync((int?)null);
|
||||||
|
data.Setup(d => d.GetLocationCountAsync(It.IsAny<CancellationToken>())).ReturnsAsync(0);
|
||||||
|
|
||||||
|
var items = new List<Dictionary<string, string?>>
|
||||||
|
{
|
||||||
|
new() { ["work_order_id"] = "W1", ["wo_status"] = "completed", ["severity"] = "2" }
|
||||||
|
};
|
||||||
|
external.Setup(e => e.ScanWorkOrdersAsync(It.IsAny<CancellationToken>())).Returns(items.ToAsyncEnumerable());
|
||||||
|
|
||||||
|
await new SyncService(data.Object, external.Object).SyncWorkOrdersAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
data.Verify(d => d.EnqueueWorkOrder(It.Is<WorkOrder>(w => w.Status == "Done" && w.Priority == "Sev 2" && w.Severity == "2")), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SyncComments_SkipsExistingAndOrphans()
|
||||||
|
{
|
||||||
|
var data = new Mock<ISyncDataService>();
|
||||||
|
var external = new Mock<ISyncExternalSource>();
|
||||||
|
var wo = new WorkOrder { Id = 10, ExternalWorkOrderId = "WO-1" };
|
||||||
|
|
||||||
|
data.Setup(d => d.CommentExistsByExternalIdAsync("C-EXISTS", It.IsAny<CancellationToken>())).ReturnsAsync(true);
|
||||||
|
data.Setup(d => d.CommentExistsByExternalIdAsync("C-NEW", It.IsAny<CancellationToken>())).ReturnsAsync(false);
|
||||||
|
data.Setup(d => d.CommentExistsByExternalIdAsync("C-ORPHAN", It.IsAny<CancellationToken>())).ReturnsAsync(false);
|
||||||
|
data.Setup(d => d.GetWorkOrderByExternalIdAsync("WO-1", It.IsAny<CancellationToken>())).ReturnsAsync(wo);
|
||||||
|
data.Setup(d => d.GetWorkOrderByExternalIdAsync("WO-MISSING", It.IsAny<CancellationToken>())).ReturnsAsync((WorkOrder?)null);
|
||||||
|
|
||||||
|
var items = new List<Dictionary<string, string?>>
|
||||||
|
{
|
||||||
|
new() { ["comment_id"] = "C-EXISTS", ["work_order_id"] = "WO-1" },
|
||||||
|
new() { ["comment_id"] = "C-NEW", ["work_order_id"] = "WO-1", ["text"] = "Hello" },
|
||||||
|
new() { ["comment_id"] = "C-ORPHAN", ["work_order_id"] = "WO-MISSING" }
|
||||||
|
};
|
||||||
|
external.Setup(e => e.ScanCommentsAsync(It.IsAny<CancellationToken>())).Returns(items.ToAsyncEnumerable());
|
||||||
|
|
||||||
|
var result = await new SyncService(data.Object, external.Object).SyncCommentsAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Synced);
|
||||||
|
Assert.Equal(1, result.Created);
|
||||||
|
Assert.Equal(2, result.Skipped);
|
||||||
|
data.Verify(d => d.EnqueueComment(It.Is<Comments>(c => c.ExternalCommentId == "C-NEW")), Times.Once);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkOrderDispatchServiceBehaviorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task VerifyDispatch_ThrowsWhenChecklistIncomplete()
|
||||||
|
{
|
||||||
|
var dispatchData = CreateVerifyDispatchMocks(
|
||||||
|
out var dispatch,
|
||||||
|
incompleteCount: 1,
|
||||||
|
checklistComplete: false,
|
||||||
|
hasCustomerSignoff: true,
|
||||||
|
hasVendorSignoff: true);
|
||||||
|
dispatch.Id = 1;
|
||||||
|
|
||||||
|
var service = CreateDispatchService(dispatchData);
|
||||||
|
var ex = await Assert.ThrowsAsync<DispatchVerificationException>(() => service.VerifyDispatchAsync(1, "user1"));
|
||||||
|
Assert.Contains("1 checklist item(s) incomplete", ex.Missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task VerifyDispatch_ThrowsWhenVendorSignoffMissing()
|
||||||
|
{
|
||||||
|
var dispatchData = CreateVerifyDispatchMocks(
|
||||||
|
out var dispatch,
|
||||||
|
incompleteCount: 0,
|
||||||
|
checklistComplete: true,
|
||||||
|
hasCustomerSignoff: true,
|
||||||
|
hasVendorSignoff: false);
|
||||||
|
dispatch.Id = 1;
|
||||||
|
|
||||||
|
var service = CreateDispatchService(dispatchData);
|
||||||
|
var ex = await Assert.ThrowsAsync<DispatchVerificationException>(() => service.VerifyDispatchAsync(1, "user1"));
|
||||||
|
Assert.Contains("Missing Vendor sign-off", ex.Missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task VerifyDispatch_SucceedsWhenAllComplete()
|
||||||
|
{
|
||||||
|
var dispatchData = CreateVerifyDispatchMocks(
|
||||||
|
out var dispatch,
|
||||||
|
incompleteCount: 0,
|
||||||
|
checklistComplete: true,
|
||||||
|
hasCustomerSignoff: true,
|
||||||
|
hasVendorSignoff: true);
|
||||||
|
dispatch.Id = 1;
|
||||||
|
|
||||||
|
var userMock = new Mock<IUserDataService>();
|
||||||
|
userMock.Setup(u => u.GetByIdAsync("user1")).ReturnsAsync(new ApplicationUser { FirstName = "Jane", LastName = "Doe" });
|
||||||
|
|
||||||
|
var service = CreateDispatchService(dispatchData, userMock: userMock);
|
||||||
|
var result = await service.VerifyDispatchAsync(1, "user1");
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("Dispatch verified", result!.Message);
|
||||||
|
Assert.Equal("Verified", dispatch.Status);
|
||||||
|
Assert.Equal("Jane Doe", dispatch.VerifiedBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mock<IDispatchDataService> CreateVerifyDispatchMocks(
|
||||||
|
out Dispatch dispatch,
|
||||||
|
int incompleteCount,
|
||||||
|
bool checklistComplete,
|
||||||
|
bool hasCustomerSignoff,
|
||||||
|
bool hasVendorSignoff)
|
||||||
|
{
|
||||||
|
dispatch = new Dispatch { Id = 1, DispatchNumber = "DSP-00001", WorkOrderId = 10 };
|
||||||
|
var mock = new Mock<IDispatchDataService>();
|
||||||
|
mock.Setup(d => d.GetByIdAsync(1)).ReturnsAsync(dispatch);
|
||||||
|
|
||||||
|
var checklist = checklistComplete
|
||||||
|
? new List<DispatchChecklistItem> { new() { IsCompleted = true } }
|
||||||
|
: new List<DispatchChecklistItem> { new() { IsCompleted = false } };
|
||||||
|
mock.Setup(d => d.GetChecklistItemsAsync(1)).ReturnsAsync(checklist);
|
||||||
|
|
||||||
|
var signoffs = new List<DispatchSignoff>();
|
||||||
|
if (hasCustomerSignoff) signoffs.Add(new DispatchSignoff { SignoffType = "Customer" });
|
||||||
|
if (hasVendorSignoff) signoffs.Add(new DispatchSignoff { SignoffType = "Vendor" });
|
||||||
|
mock.Setup(d => d.GetSignoffsAsync(1)).ReturnsAsync(signoffs);
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorkOrderDispatchService CreateDispatchService(
|
||||||
|
Mock<IDispatchDataService>? dispatchDataMock = null,
|
||||||
|
Mock<IUserDataService>? userMock = null)
|
||||||
|
{
|
||||||
|
return new WorkOrderDispatchService(
|
||||||
|
Mock.Of<IWorkOrderDataService>(),
|
||||||
|
(dispatchDataMock ?? new Mock<IDispatchDataService>()).Object,
|
||||||
|
Mock.Of<IVendorDataService>(),
|
||||||
|
(userMock ?? new Mock<IUserDataService>()).Object,
|
||||||
|
Mock.Of<ITaskListTemplateDataService>(),
|
||||||
|
Mock.Of<ICommentDataService>(),
|
||||||
|
Mock.Of<IDispatchEmailPort>(),
|
||||||
|
Mock.Of<IVendorTokenPort>(),
|
||||||
|
Mock.Of<IConfiguration>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkOrderServiceBehaviorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ChangeStatus_AddsAuditWhenStatusDiffers()
|
||||||
|
{
|
||||||
|
var wo = new WorkOrder { Id = 5, Status = "Open" };
|
||||||
|
var woData = new Mock<IWorkOrderDataService>();
|
||||||
|
woData.Setup(d => d.GetByIdAsync(5)).ReturnsAsync(wo);
|
||||||
|
|
||||||
|
var service = CreateWorkOrderService(woData);
|
||||||
|
var result = await service.ChangeStatusAsync(5, "In Progress", "user1");
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("In Progress", wo.Status);
|
||||||
|
woData.Verify(d => d.AddAuditLogAsync(It.Is<WorkOrderAuditLog>(a =>
|
||||||
|
a.FieldName == "Status" && a.OldValue == "Open" && a.NewValue == "In Progress")), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChangeStatus_DoesNotAuditWhenStatusSame()
|
||||||
|
{
|
||||||
|
var wo = new WorkOrder { Id = 5, Status = "Open" };
|
||||||
|
var woData = new Mock<IWorkOrderDataService>();
|
||||||
|
woData.Setup(d => d.GetByIdAsync(5)).ReturnsAsync(wo);
|
||||||
|
|
||||||
|
await CreateWorkOrderService(woData).ChangeStatusAsync(5, "Open", "user1");
|
||||||
|
|
||||||
|
woData.Verify(d => d.AddAuditLogAsync(It.IsAny<WorkOrderAuditLog>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteWorkOrderCascade_UnlinksQuotesAndDeletesComments()
|
||||||
|
{
|
||||||
|
var woData = new Mock<IWorkOrderDataService>();
|
||||||
|
woData.Setup(d => d.ExistsAsync(5)).ReturnsAsync(true);
|
||||||
|
|
||||||
|
var commentData = new Mock<ICommentDataService>();
|
||||||
|
commentData.Setup(c => c.GetByWorkOrderIdAsync(5))
|
||||||
|
.ReturnsAsync(new List<Comments> { new() { Id = 1 }, new() { Id = 2 } });
|
||||||
|
|
||||||
|
var quotesData = new Mock<IQuotesDataService>();
|
||||||
|
|
||||||
|
var result = await CreateWorkOrderService(woData, commentData, quotesData).DeleteWorkOrderCascadeAsync(5, "user1");
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
quotesData.Verify(q => q.UnlinkFromWorkOrderAsync(5), Times.Once);
|
||||||
|
commentData.Verify(c => c.DeleteAsync(1), Times.Once);
|
||||||
|
commentData.Verify(c => c.DeleteAsync(2), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorkOrderService CreateWorkOrderService(
|
||||||
|
Mock<IWorkOrderDataService>? woData = null,
|
||||||
|
Mock<ICommentDataService>? commentData = null,
|
||||||
|
Mock<IQuotesDataService>? quotesData = null)
|
||||||
|
{
|
||||||
|
return new WorkOrderService(
|
||||||
|
(woData ?? new Mock<IWorkOrderDataService>()).Object,
|
||||||
|
(commentData ?? new Mock<ICommentDataService>()).Object,
|
||||||
|
Mock.Of<IUserDataService>(),
|
||||||
|
(quotesData ?? new Mock<IQuotesDataService>()).Object,
|
||||||
|
Mock.Of<IFileStoragePort>(),
|
||||||
|
Mock.Of<ICreateWorkOrderValidation>(),
|
||||||
|
Mock.Of<IUpdateWorkOrderValidation>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
using Api.SeaHavenIndustries.DTOs;
|
using Api.SeaHavenIndustries.DTOs;
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Data.SqlClient;
|
using SeaHaven.Services.DTOs;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using SeaHaven.Services.Interfaces;
|
||||||
using System.Linq;
|
|
||||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
|
||||||
|
|
||||||
namespace Api.SeaHavenIndustries.Controllers
|
namespace Api.SeaHavenIndustries.Controllers
|
||||||
{
|
{
|
||||||
|
|
@ -15,52 +12,21 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
[Route("api/Quotes")]
|
[Route("api/Quotes")]
|
||||||
public class QuotesController : Controller
|
public class QuotesController : Controller
|
||||||
{
|
{
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly IQuotesService _quotesService;
|
||||||
private readonly ApplicationDbContext _db;
|
|
||||||
private readonly IWebHostEnvironment _webHostEnvironment;
|
public QuotesController(IQuotesService quotesService)
|
||||||
IHttpContextAccessor _httpContext;
|
|
||||||
public QuotesController(UserManager<ApplicationUser> userManager, ApplicationDbContext db, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContext)
|
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_quotesService = quotesService;
|
||||||
_db = db;
|
|
||||||
_webHostEnvironment = webHostEnvironment;
|
|
||||||
_httpContext = httpContext;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("AddQuote")]
|
[Route("AddQuote")]
|
||||||
public async Task<IActionResult> AddQuote([FromBody] Quote_DTO model)
|
public async Task<IActionResult> AddQuote([FromBody] Quote_DTO model, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Random rnd = new Random();
|
await _quotesService.CreateQuoteAsync(MapToCreateInput(model), cancellationToken);
|
||||||
int id = rnd.Next(1, 99999);
|
|
||||||
Quotes quotes = new Quotes();
|
|
||||||
quotes.QuotId = id.ToString();
|
|
||||||
quotes.ContactId = model.ContactId;
|
|
||||||
quotes.PO = model.PO;
|
|
||||||
quotes.TT = model.TT;
|
|
||||||
quotes.ProjectName = model.ProjectName;
|
|
||||||
quotes.Date = DateTime.Parse(model.Date);
|
|
||||||
quotes.WorkorderId = model.WorkorderId;
|
|
||||||
quotes.SiteName = model.SiteName;
|
|
||||||
quotes.SiteId = model.SiteId;
|
|
||||||
quotes.BillingAddress = model.BillingAddress;
|
|
||||||
quotes.LineItems = quotes.LineItems == null ? new List<QuotesLineItems>() : quotes.LineItems;
|
|
||||||
if (model.LineItems != null && model.LineItems.Any())
|
|
||||||
{
|
|
||||||
foreach (var lineItemDto in model.LineItems)
|
|
||||||
{
|
|
||||||
QuotesLineItems lineItem = new QuotesLineItems()
|
|
||||||
{
|
|
||||||
LineitemNumber = lineItemDto.LineitemNumber,
|
|
||||||
Lineitem = lineItemDto.Lineitem,
|
|
||||||
Descriptions = lineItemDto.Descriptions
|
|
||||||
};
|
|
||||||
quotes.LineItems.Add(lineItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_db.Quotes.Add(quotes);
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -71,50 +37,14 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
|
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
[Route("EditQuote")]
|
[Route("EditQuote")]
|
||||||
public async Task<IActionResult> EditQuote([FromBody] EditQuote_DTO model)
|
public async Task<IActionResult> EditQuote([FromBody] EditQuote_DTO model, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var quotes = _db.Quotes.Where(w => w.Id == model.id).FirstOrDefault();
|
var updated = await _quotesService.UpdateQuoteAsync(MapToUpdateInput(model), cancellationToken);
|
||||||
if (quotes != null)
|
if (!updated)
|
||||||
{
|
|
||||||
quotes.QuotId = model.QuotId;
|
|
||||||
quotes.ContactId = model.ContactId;
|
|
||||||
quotes.PO = model.PO;
|
|
||||||
quotes.TT = model.TT;
|
|
||||||
quotes.ProjectName = model.ProjectName;
|
|
||||||
quotes.Date = DateTime.Parse(model.Date);
|
|
||||||
quotes.WorkorderId = model.WorkorderId;
|
|
||||||
quotes.SiteName = model.SiteName;
|
|
||||||
quotes.SiteId = model.SiteId;
|
|
||||||
quotes.BillingAddress = model.BillingAddress;
|
|
||||||
|
|
||||||
var existQuoteLine = _db.QuotesLines.Where(w => w.QuoteId == model.id).ToList();
|
|
||||||
if (existQuoteLine.Count() > 0)
|
|
||||||
{
|
|
||||||
_db.QuotesLines.RemoveRange(existQuoteLine);
|
|
||||||
}
|
|
||||||
quotes.LineItems = quotes.LineItems == null ? new List<QuotesLineItems>() : quotes.LineItems;
|
|
||||||
if (model.LineItems != null && model.LineItems.Any())
|
|
||||||
{
|
|
||||||
foreach (var lineItemDto in model.LineItems)
|
|
||||||
{
|
|
||||||
QuotesLineItems lineItem = new QuotesLineItems()
|
|
||||||
{
|
|
||||||
LineitemNumber = lineItemDto.LineitemNumber,
|
|
||||||
Lineitem = lineItemDto.Lineitem,
|
|
||||||
Descriptions = lineItemDto.Descriptions
|
|
||||||
};
|
|
||||||
quotes.LineItems.Add(lineItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_db.Update(quotes);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return BadRequest("ID not matched!");
|
return BadRequest("ID not matched!");
|
||||||
}
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -125,26 +55,15 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
|
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
[Route("DeleteQuote")]
|
[Route("DeleteQuote")]
|
||||||
public IActionResult DeleteQuote(EditQuote_DTO model)
|
public async Task<IActionResult> DeleteQuote(EditQuote_DTO model, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
int id = model.id;
|
int id = model.id;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var data = _db.Quotes.Where(w => w.Id == id).FirstOrDefault();
|
var deleted = await _quotesService.DeleteQuoteAsync(id, cancellationToken);
|
||||||
if (data != null)
|
if (!deleted)
|
||||||
{
|
|
||||||
var existlineitem = _db.QuotesLines.Where(w => w.QuoteId == id).ToList();
|
|
||||||
if (existlineitem.Any())
|
|
||||||
{
|
|
||||||
_db.QuotesLines.RemoveRange(existlineitem);
|
|
||||||
}
|
|
||||||
_db.Quotes.Remove(data);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return BadRequest("ID not matched!");
|
return BadRequest("ID not matched!");
|
||||||
}
|
|
||||||
_db.SaveChanges();
|
|
||||||
return Ok(new DataResponse { Message = "Removed Successfully", Status = "200" });
|
return Ok(new DataResponse { Message = "Removed Successfully", Status = "200" });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -155,38 +74,18 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("GetQuoutes")]
|
[Route("GetQuoutes")]
|
||||||
public IActionResult GetQuotes(string search = "", int page = 1, int pageSize = 10)
|
public async Task<IActionResult> GetQuotes(string search = "", int page = 1, int pageSize = 10, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var totalCount = _db.Quotes.Count();
|
var result = await _quotesService.GetQuotesPagedAsync(page, pageSize, search, cancellationToken);
|
||||||
var data = _db.Quotes.Include(s => s.WorkOrder).AsNoTracking().Where(w => (w.QuotId ?? "").ToLower().Contains(search.ToLower()) || (w.SiteName ?? "").ToLower().Contains(search.ToLower()) || (w.WorkOrder != null && (w.WorkOrder.WorkerOrderTitle ?? "").ToLower().Contains(search.ToLower()))).Select(s => new
|
|
||||||
{
|
|
||||||
s.Id,
|
|
||||||
QuoteId = "QO-" + s.QuotId,
|
|
||||||
WorkOrder = s.WorkOrder != null ? s.WorkOrder.WorkerOrderNumber : "",
|
|
||||||
s.ProjectName,
|
|
||||||
TotalAmount = "$" + (s.LineItems != null ? s.LineItems.Sum(l => l.Lineitem ?? 0) : 0).ToString(),
|
|
||||||
s.SiteName,
|
|
||||||
s.SiteId,
|
|
||||||
s.PO,
|
|
||||||
s.TT,
|
|
||||||
s.BillingAddress,
|
|
||||||
s.Status,
|
|
||||||
s.ContactId,
|
|
||||||
s.Date,
|
|
||||||
lineitems = s.LineItems.Select(q => new { q.Descriptions, q.Lineitem, q.LineitemNumber}),
|
|
||||||
}).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize).ToList();
|
|
||||||
|
|
||||||
// Calculate the number of pages
|
var totalPages = (int)Math.Ceiling(result.TotalCount / (double)pageSize);
|
||||||
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
|
||||||
|
|
||||||
// Create a view model containing the paged items and pagination metadata
|
|
||||||
var viewModel = new Pagination_DTO
|
var viewModel = new Pagination_DTO
|
||||||
{
|
{
|
||||||
Data = data,
|
Data = result.Items,
|
||||||
PageNumber = page,
|
PageNumber = page,
|
||||||
PageSize = pageSize,
|
PageSize = pageSize,
|
||||||
TotalCount = totalCount,
|
TotalCount = result.TotalCount,
|
||||||
TotalPages = totalPages
|
TotalPages = totalPages
|
||||||
};
|
};
|
||||||
return Ok(viewModel);
|
return Ok(viewModel);
|
||||||
|
|
@ -194,66 +93,85 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("GetQuoutesdetailsAgainstWorkOrder")]
|
[Route("GetQuoutesdetailsAgainstWorkOrder")]
|
||||||
public async Task<IActionResult> GetQuoutesdetailsAgainstWorkOrder(int woId)
|
public async Task<IActionResult> GetQuoutesdetailsAgainstWorkOrder(int woId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var data = await _db.workOrders.Include(s => s.Locations).Where(w => w.Id == woId).Select(s => new
|
var data = await _quotesService.GetWorkOrderQuoteDetailsAsync(woId, cancellationToken);
|
||||||
{
|
|
||||||
s.Id,
|
|
||||||
s.PO,
|
|
||||||
s.TT,
|
|
||||||
Title = s.Locations != null ? s.Locations.Title : "",
|
|
||||||
Name = s.Locations != null ? s.Locations.Name : ""
|
|
||||||
}).ToListAsync();
|
|
||||||
return Ok(data);
|
return Ok(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("GetQuoutesById")]
|
[Route("GetQuoutesById")]
|
||||||
public async Task<IActionResult> GetQuoutesById(int id)
|
public async Task<IActionResult> GetQuoutesById(int id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var data = await _db.Quotes.Where(s => s.Id == id).Include(s => s.WorkOrder).Select(s => new
|
var data = await _quotesService.GetQuoteDetailsByIdAsync(id, cancellationToken);
|
||||||
{
|
|
||||||
s.Id,
|
|
||||||
s.QuotId,
|
|
||||||
s.Date,
|
|
||||||
Name = s.Contacts != null ? (s.Contacts.FirstName + " " + s.Contacts.LastName).Trim() : "",
|
|
||||||
s.PO,
|
|
||||||
s.TT,
|
|
||||||
s.SiteName,
|
|
||||||
s.ProjectName,
|
|
||||||
s.SiteId,
|
|
||||||
s.BillingAddress,
|
|
||||||
s.ShippingAddress,
|
|
||||||
lineitems = s.LineItems.Select(q => new { q.Descriptions, q.Lineitem }),
|
|
||||||
TotalAmount = "$" + (s.LineItems != null ? s.LineItems.Sum(l => l.Lineitem ?? 0) : 0).ToString()
|
|
||||||
}).FirstOrDefaultAsync();
|
|
||||||
return Ok(data);
|
return Ok(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("GetReporting")]
|
[Route("GetReporting")]
|
||||||
public async Task<IActionResult> GetReporting()
|
public async Task<IActionResult> GetReporting(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var totalCreatedWorkOrder = await _db.workOrders.CountAsync();
|
var data = await _quotesService.GetReportingAsync(cancellationToken);
|
||||||
var totalCompleted = _db.workOrders.Count(w => w.Status == "Done");
|
|
||||||
var totalSev1 = _db.workOrders.Count(w => w.Priority == "Sev 1");
|
|
||||||
var totalSev2 = _db.workOrders.Count(w => w.Priority == "Sev 2");
|
|
||||||
var totalSev3 = _db.workOrders.Count(w => w.Priority == "Sev 3");
|
|
||||||
var totalSev4 = _db.workOrders.Count(w => w.Priority == "Sev 4");
|
|
||||||
var totalSev5 = _db.workOrders.Count(w => w.Priority == "Sev 5");
|
|
||||||
|
|
||||||
var data = new Reporting_DTO
|
var reportingDto = new Reporting_DTO
|
||||||
{
|
{
|
||||||
TotalCreatedWorkOrder = totalCreatedWorkOrder,
|
TotalCreatedWorkOrder = data.TotalCreatedWorkOrder,
|
||||||
TotalCompleted = totalCompleted,
|
TotalCompleted = data.TotalCompleted,
|
||||||
TotalSev1 = totalSev1,
|
TotalSev1 = data.TotalSev1,
|
||||||
TotalSev2 = totalSev2,
|
TotalSev2 = data.TotalSev2,
|
||||||
TotalSev3 = totalSev3,
|
TotalSev3 = data.TotalSev3,
|
||||||
TotalSev4 = totalSev4,
|
TotalSev4 = data.TotalSev4,
|
||||||
TotalSev5 = totalSev5
|
TotalSev5 = data.TotalSev5
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(data);
|
return Ok(reportingDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CreateQuoteInput MapToCreateInput(Quote_DTO dto)
|
||||||
|
{
|
||||||
|
return new CreateQuoteInput
|
||||||
|
{
|
||||||
|
QuotId = dto.QuotId,
|
||||||
|
ContactId = dto.ContactId,
|
||||||
|
PO = dto.PO,
|
||||||
|
TT = dto.TT,
|
||||||
|
ProjectName = dto.ProjectName,
|
||||||
|
Date = dto.Date != null ? DateTime.Parse(dto.Date) : null,
|
||||||
|
WorkorderId = dto.WorkorderId,
|
||||||
|
SiteName = dto.SiteName,
|
||||||
|
SiteId = dto.SiteId,
|
||||||
|
BillingAddress = dto.BillingAddress,
|
||||||
|
LineItems = dto.LineItems?.Select(li => new QuoteLineItemInput
|
||||||
|
{
|
||||||
|
LineitemNumber = li.LineitemNumber,
|
||||||
|
Lineitem = li.Lineitem,
|
||||||
|
Descriptions = li.Descriptions
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UpdateQuoteInput MapToUpdateInput(EditQuote_DTO dto)
|
||||||
|
{
|
||||||
|
return new UpdateQuoteInput
|
||||||
|
{
|
||||||
|
Id = dto.id,
|
||||||
|
QuotId = dto.QuotId,
|
||||||
|
ContactId = dto.ContactId,
|
||||||
|
PO = dto.PO,
|
||||||
|
TT = dto.TT,
|
||||||
|
ProjectName = dto.ProjectName,
|
||||||
|
Date = dto.Date != null ? DateTime.Parse(dto.Date) : null,
|
||||||
|
WorkorderId = dto.WorkorderId,
|
||||||
|
SiteName = dto.SiteName,
|
||||||
|
SiteId = dto.SiteId,
|
||||||
|
BillingAddress = dto.BillingAddress,
|
||||||
|
LineItems = dto.LineItems?.Select(li => new QuoteLineItemInput
|
||||||
|
{
|
||||||
|
LineitemNumber = li.LineitemNumber,
|
||||||
|
Lineitem = li.Lineitem,
|
||||||
|
Descriptions = li.Descriptions
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
using Amazon.DynamoDBv2;
|
|
||||||
using Amazon.DynamoDBv2.Model;
|
|
||||||
using Data.SeaHavenIndustries;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
namespace Api.SeaHavenIndustries.Controllers
|
namespace Api.SeaHavenIndustries.Controllers
|
||||||
{
|
{
|
||||||
|
|
@ -12,412 +9,60 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class SyncController : Controller
|
public class SyncController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _db;
|
private readonly ISyncService _syncService;
|
||||||
private readonly AmazonDynamoDBClient _dynamo;
|
|
||||||
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(ISyncService syncService)
|
||||||
{
|
{
|
||||||
_db = db;
|
_syncService = syncService;
|
||||||
_dynamo = new AmazonDynamoDBClient(Amazon.RegionEndpoint.USEast1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("WorkOrders")]
|
[HttpPost("WorkOrders")]
|
||||||
public async Task<IActionResult> SyncWorkOrders()
|
public async Task<IActionResult> SyncWorkOrders(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var synced = 0;
|
var result = await _syncService.SyncWorkOrdersAsync(cancellationToken);
|
||||||
var created = 0;
|
return Ok(result);
|
||||||
var updated = 0;
|
|
||||||
var locationsCreated = 0;
|
|
||||||
|
|
||||||
var lastInternalWO = await _db.workOrders
|
|
||||||
.Where(w => w.InternalWONumber != null && w.InternalWONumber != "")
|
|
||||||
.OrderByDescending(w => w.InternalWONumber)
|
|
||||||
.Select(w => w.InternalWONumber)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
int nextInternal = 10000001;
|
|
||||||
if (lastInternalWO != null && int.TryParse(lastInternalWO, out var parsed))
|
|
||||||
nextInternal = parsed + 1;
|
|
||||||
|
|
||||||
Dictionary<string, AttributeValue>? lastKey = null;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
var request = new ScanRequest
|
|
||||||
{
|
|
||||||
TableName = WO_TABLE,
|
|
||||||
Limit = 100,
|
|
||||||
ExclusiveStartKey = lastKey
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await _dynamo.ScanAsync(request);
|
|
||||||
|
|
||||||
foreach (var item in response.Items)
|
|
||||||
{
|
|
||||||
var externalId = GetString(item, "work_order_id");
|
|
||||||
if (string.IsNullOrEmpty(externalId)) continue;
|
|
||||||
|
|
||||||
var existing = await _db.workOrders
|
|
||||||
.FirstOrDefaultAsync(w => w.ExternalWorkOrderId == externalId);
|
|
||||||
|
|
||||||
var locationId = await ResolveLocationId(
|
|
||||||
GetString(item, "site_code"),
|
|
||||||
GetString(item, "building"),
|
|
||||||
GetString(item, "address"));
|
|
||||||
|
|
||||||
if (locationId.HasValue && !locationsCreated.Equals(0))
|
|
||||||
locationsCreated++;
|
|
||||||
|
|
||||||
if (existing == null)
|
|
||||||
{
|
|
||||||
var wo = new WorkOrder
|
|
||||||
{
|
|
||||||
InternalWONumber = (nextInternal++).ToString("D8"),
|
|
||||||
ExternalWorkOrderId = externalId,
|
|
||||||
WorkerOrderNumber = externalId,
|
|
||||||
WorkerOrderTitle = GetString(item, "description"),
|
|
||||||
Description = GetString(item, "description"),
|
|
||||||
Status = MapStatus(GetString(item, "wo_status")),
|
|
||||||
Priority = MapSeverityToPriority(GetString(item, "severity")),
|
|
||||||
Severity = GetString(item, "severity"),
|
|
||||||
Customer = GetString(item, "customer"),
|
|
||||||
SiteCode = GetString(item, "site_code"),
|
|
||||||
Building = GetString(item, "building"),
|
|
||||||
LocationId = locationId,
|
|
||||||
DueDate = ParseDate(GetString(item, "due_date")),
|
|
||||||
DateReported = ParseDate(GetString(item, "date_reported")),
|
|
||||||
ScheduledStart = ParseDate(GetString(item, "scheduled_start")),
|
|
||||||
AssignTo = null,
|
|
||||||
SourceEmailS3Key = GetString(item, "source_email_s3_key"),
|
|
||||||
CreatedDate = ParseDate(GetString(item, "created_at")) ?? DateTime.UtcNow,
|
|
||||||
istemplate = false
|
|
||||||
};
|
|
||||||
_db.workOrders.Add(wo);
|
|
||||||
created++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
existing.WorkerOrderTitle = GetString(item, "description") ?? existing.WorkerOrderTitle;
|
|
||||||
existing.Description = GetString(item, "description") ?? existing.Description;
|
|
||||||
existing.Status = MapStatus(GetString(item, "wo_status")) ?? existing.Status;
|
|
||||||
existing.Priority = MapSeverityToPriority(GetString(item, "severity")) ?? existing.Priority;
|
|
||||||
existing.Severity = GetString(item, "severity") ?? existing.Severity;
|
|
||||||
existing.SiteCode = GetString(item, "site_code") ?? existing.SiteCode;
|
|
||||||
existing.Building = GetString(item, "building") ?? existing.Building;
|
|
||||||
existing.LocationId = locationId ?? existing.LocationId;
|
|
||||||
existing.DueDate = ParseDate(GetString(item, "due_date")) ?? existing.DueDate;
|
|
||||||
existing.DateReported = ParseDate(GetString(item, "date_reported")) ?? existing.DateReported;
|
|
||||||
existing.ScheduledStart = ParseDate(GetString(item, "scheduled_start")) ?? existing.ScheduledStart;
|
|
||||||
existing.SourceEmailS3Key = GetString(item, "source_email_s3_key") ?? existing.SourceEmailS3Key;
|
|
||||||
updated++;
|
|
||||||
}
|
|
||||||
|
|
||||||
synced++;
|
|
||||||
|
|
||||||
if (synced % 100 == 0)
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
lastKey = response.LastEvaluatedKey;
|
|
||||||
|
|
||||||
} while (lastKey != null && lastKey.Count > 0);
|
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
synced,
|
|
||||||
created,
|
|
||||||
updated,
|
|
||||||
locationsCreated = await _db.Locations.CountAsync()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("Comments")]
|
[HttpPost("Comments")]
|
||||||
public async Task<IActionResult> SyncComments()
|
public async Task<IActionResult> SyncComments(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var synced = 0;
|
var result = await _syncService.SyncCommentsAsync(cancellationToken);
|
||||||
var created = 0;
|
return Ok(result);
|
||||||
var skipped = 0;
|
|
||||||
Dictionary<string, AttributeValue>? lastKey = null;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
var request = new ScanRequest
|
|
||||||
{
|
|
||||||
TableName = COMMENTS_TABLE,
|
|
||||||
Limit = 100,
|
|
||||||
ExclusiveStartKey = lastKey
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await _dynamo.ScanAsync(request);
|
|
||||||
|
|
||||||
foreach (var item in response.Items)
|
|
||||||
{
|
|
||||||
var externalCommentId = GetString(item, "comment_id");
|
|
||||||
var externalWoId = GetString(item, "work_order_id");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(externalCommentId) || string.IsNullOrEmpty(externalWoId))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var alreadyExists = await _db.Comments
|
|
||||||
.AnyAsync(c => c.ExternalCommentId == externalCommentId);
|
|
||||||
|
|
||||||
if (alreadyExists)
|
|
||||||
{
|
|
||||||
skipped++;
|
|
||||||
synced++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var workOrder = await _db.workOrders
|
|
||||||
.FirstOrDefaultAsync(w => w.ExternalWorkOrderId == externalWoId);
|
|
||||||
|
|
||||||
if (workOrder == null)
|
|
||||||
{
|
|
||||||
skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var comment = new Comments
|
|
||||||
{
|
|
||||||
ExternalCommentId = externalCommentId,
|
|
||||||
WorkerOrderId = workOrder.Id,
|
|
||||||
Commenttext = GetString(item, "text"),
|
|
||||||
Commenter = GetString(item, "commenter"),
|
|
||||||
RecordType = GetString(item, "record_type"),
|
|
||||||
CommentType = "customer",
|
|
||||||
CreatedDate = ParseDate(GetString(item, "created_at")) ?? DateTime.UtcNow,
|
|
||||||
};
|
|
||||||
|
|
||||||
_db.Comments.Add(comment);
|
|
||||||
created++;
|
|
||||||
synced++;
|
|
||||||
|
|
||||||
if (synced % 100 == 0)
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
lastKey = response.LastEvaluatedKey;
|
|
||||||
|
|
||||||
} while (lastKey != null && lastKey.Count > 0);
|
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return Ok(new { synced, created, skipped });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("BackfillInternalWONumbers")]
|
[HttpPost("BackfillInternalWONumbers")]
|
||||||
public async Task<IActionResult> BackfillInternalWONumbers()
|
public async Task<IActionResult> BackfillInternalWONumbers(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var wosMissing = await _db.workOrders
|
var result = await _syncService.BackfillInternalWONumbersAsync(cancellationToken);
|
||||||
.Where(w => w.InternalWONumber == null || w.InternalWONumber == "")
|
return Ok(result);
|
||||||
.OrderBy(w => w.Id)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
if (wosMissing.Count == 0)
|
|
||||||
return Ok(new { updated = 0 });
|
|
||||||
|
|
||||||
var lastWO = await _db.workOrders
|
|
||||||
.Where(w => w.InternalWONumber != null && w.InternalWONumber != "")
|
|
||||||
.OrderByDescending(w => w.InternalWONumber)
|
|
||||||
.Select(w => w.InternalWONumber)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
int next = 10000001;
|
|
||||||
if (lastWO != null && int.TryParse(lastWO, out var lastNum))
|
|
||||||
next = lastNum + 1;
|
|
||||||
|
|
||||||
foreach (var wo in wosMissing)
|
|
||||||
{
|
|
||||||
wo.InternalWONumber = next.ToString("D8");
|
|
||||||
next++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return Ok(new { updated = wosMissing.Count });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("BackfillDispatchNumbers")]
|
[HttpPost("BackfillDispatchNumbers")]
|
||||||
public async Task<IActionResult> BackfillDispatchNumbers()
|
public async Task<IActionResult> BackfillDispatchNumbers(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var missing = await _db.Dispatches
|
var result = await _syncService.BackfillDispatchNumbersAsync(cancellationToken);
|
||||||
.Where(d => d.DispatchNumber == null || d.DispatchNumber == "")
|
return Ok(result);
|
||||||
.OrderBy(d => d.Id)
|
|
||||||
.ToListAsync();
|
|
||||||
if (missing.Count == 0) return Ok(new { updated = 0 });
|
|
||||||
|
|
||||||
int next = 1;
|
|
||||||
var last = await _db.Dispatches
|
|
||||||
.Where(d => d.DispatchNumber != null && d.DispatchNumber != "")
|
|
||||||
.OrderByDescending(d => d.DispatchNumber)
|
|
||||||
.Select(d => d.DispatchNumber)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (last != null && last.StartsWith("DSP-") && int.TryParse(last.Substring(4), out var n))
|
|
||||||
next = n + 1;
|
|
||||||
|
|
||||||
foreach (var d in missing)
|
|
||||||
{
|
|
||||||
d.DispatchNumber = $"DSP-{next:D5}";
|
|
||||||
next++;
|
|
||||||
}
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return Ok(new { updated = missing.Count });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("VendorReplies")]
|
[HttpPost("VendorReplies")]
|
||||||
public async Task<IActionResult> SyncVendorReplies()
|
public async Task<IActionResult> SyncVendorReplies(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var synced = 0;
|
var result = await _syncService.SyncVendorRepliesAsync(cancellationToken);
|
||||||
var created = 0;
|
return Ok(result);
|
||||||
var skipped = 0;
|
|
||||||
Dictionary<string, AttributeValue>? lastKey = null;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
var request = new ScanRequest
|
|
||||||
{
|
|
||||||
TableName = VENDOR_REPLIES_TABLE,
|
|
||||||
Limit = 100,
|
|
||||||
ExclusiveStartKey = lastKey
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await _dynamo.ScanAsync(request);
|
|
||||||
|
|
||||||
foreach (var item in response.Items)
|
|
||||||
{
|
|
||||||
var replyId = GetString(item, "reply_id");
|
|
||||||
var woNumber = GetString(item, "internal_wo_number");
|
|
||||||
var dispatchNumber = GetString(item, "dispatch_number");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(replyId) || string.IsNullOrEmpty(woNumber))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var alreadyExists = await _db.Comments
|
|
||||||
.AnyAsync(c => c.ExternalCommentId == replyId);
|
|
||||||
if (alreadyExists) { skipped++; synced++; continue; }
|
|
||||||
|
|
||||||
var workOrder = await _db.workOrders
|
|
||||||
.FirstOrDefaultAsync(w => w.InternalWONumber == woNumber);
|
|
||||||
if (workOrder == null) { skipped++; continue; }
|
|
||||||
|
|
||||||
int? dispatchId = null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(dispatchNumber))
|
|
||||||
{
|
|
||||||
var dispatch = await _db.Dispatches
|
|
||||||
.FirstOrDefaultAsync(d => d.DispatchNumber == dispatchNumber && d.WorkOrderId == workOrder.Id);
|
|
||||||
dispatchId = dispatch?.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
var comment = new Comments
|
|
||||||
{
|
|
||||||
ExternalCommentId = replyId,
|
|
||||||
WorkerOrderId = workOrder.Id,
|
|
||||||
DispatchId = dispatchId,
|
|
||||||
Commenttext = GetString(item, "reply_text"),
|
|
||||||
Commenter = GetString(item, "sender_email"),
|
|
||||||
CommentType = "vendor",
|
|
||||||
RecordType = "vendor_reply",
|
|
||||||
CreatedDate = ParseDate(GetString(item, "received_at")) ?? DateTime.UtcNow,
|
|
||||||
};
|
|
||||||
|
|
||||||
_db.Comments.Add(comment);
|
|
||||||
created++;
|
|
||||||
synced++;
|
|
||||||
|
|
||||||
if (synced % 50 == 0)
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
lastKey = response.LastEvaluatedKey;
|
|
||||||
} while (lastKey != null && lastKey.Count > 0);
|
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
return Ok(new { synced, created, skipped });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("BackfillCommentTypes")]
|
[HttpPost("BackfillCommentTypes")]
|
||||||
public async Task<IActionResult> BackfillCommentTypes()
|
public async Task<IActionResult> BackfillCommentTypes(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var updated = await _db.Database.ExecuteSqlRawAsync(
|
var result = await _syncService.BackfillCommentTypesAsync(cancellationToken);
|
||||||
"UPDATE Comments SET CommentType = 'customer' WHERE ExternalCommentId IS NOT NULL AND (CommentType IS NULL OR CommentType = '')");
|
return Ok(result);
|
||||||
return Ok(new { updated });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("All")]
|
[HttpPost("All")]
|
||||||
public async Task<IActionResult> SyncAll()
|
public async Task<IActionResult> SyncAll(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var woResult = await SyncWorkOrders() as OkObjectResult;
|
var result = await _syncService.SyncAllAsync(cancellationToken);
|
||||||
var commentResult = await SyncComments() as OkObjectResult;
|
return Ok(result);
|
||||||
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
workOrders = woResult?.Value,
|
|
||||||
comments = commentResult?.Value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int?> ResolveLocationId(string? siteCode, string? building, string? address)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(siteCode) && string.IsNullOrWhiteSpace(building))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var matchCode = siteCode ?? building;
|
|
||||||
var existing = await _db.Locations
|
|
||||||
.FirstOrDefaultAsync(l => l.Name == matchCode || l.Title == matchCode);
|
|
||||||
|
|
||||||
if (existing != null)
|
|
||||||
return existing.Id;
|
|
||||||
|
|
||||||
var location = new Locations
|
|
||||||
{
|
|
||||||
Name = matchCode,
|
|
||||||
Title = building,
|
|
||||||
Address1 = address,
|
|
||||||
Status = "Active"
|
|
||||||
};
|
|
||||||
|
|
||||||
_db.Locations.Add(location);
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return location.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? GetString(Dictionary<string, AttributeValue> item, string key)
|
|
||||||
{
|
|
||||||
if (item.TryGetValue(key, out var val) && val.S != null)
|
|
||||||
return val.S;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DateTime? ParseDate(string? value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
|
||||||
if (DateTime.TryParse(value, out var dt)) return dt;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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",
|
|
||||||
_ => "Open"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? MapSeverityToPriority(string? severity)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(severity)) return null;
|
|
||||||
return $"Sev {severity}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,90 @@
|
||||||
|
using Amazon.DynamoDBv2;
|
||||||
|
using Amazon.DynamoDBv2.Model;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Infrastructure
|
||||||
|
{
|
||||||
|
public class DynamoSyncExternalSource : ISyncExternalSource
|
||||||
|
{
|
||||||
|
private readonly AmazonDynamoDBClient _dynamo;
|
||||||
|
private const string WO_TABLE = "WorkOrders";
|
||||||
|
private const string COMMENTS_TABLE = "WorkOrderComments";
|
||||||
|
private const string VENDOR_REPLIES_TABLE = "VendorReplies";
|
||||||
|
|
||||||
|
public DynamoSyncExternalSource()
|
||||||
|
{
|
||||||
|
_dynamo = new AmazonDynamoDBClient(Amazon.RegionEndpoint.USEast1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<Dictionary<string, string?>> ScanWorkOrdersAsync(
|
||||||
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Dictionary<string, AttributeValue>? lastKey = null;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var request = new ScanRequest
|
||||||
|
{
|
||||||
|
TableName = WO_TABLE,
|
||||||
|
Limit = 100,
|
||||||
|
ExclusiveStartKey = lastKey
|
||||||
|
};
|
||||||
|
var response = await _dynamo.ScanAsync(request);
|
||||||
|
foreach (var item in response.Items)
|
||||||
|
yield return ConvertItem(item);
|
||||||
|
lastKey = response.LastEvaluatedKey;
|
||||||
|
} while (lastKey != null && lastKey.Count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<Dictionary<string, string?>> ScanCommentsAsync(
|
||||||
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Dictionary<string, AttributeValue>? lastKey = null;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var request = new ScanRequest
|
||||||
|
{
|
||||||
|
TableName = COMMENTS_TABLE,
|
||||||
|
Limit = 100,
|
||||||
|
ExclusiveStartKey = lastKey
|
||||||
|
};
|
||||||
|
var response = await _dynamo.ScanAsync(request);
|
||||||
|
foreach (var item in response.Items)
|
||||||
|
yield return ConvertItem(item);
|
||||||
|
lastKey = response.LastEvaluatedKey;
|
||||||
|
} while (lastKey != null && lastKey.Count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<Dictionary<string, string?>> ScanVendorRepliesAsync(
|
||||||
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Dictionary<string, AttributeValue>? lastKey = null;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var request = new ScanRequest
|
||||||
|
{
|
||||||
|
TableName = VENDOR_REPLIES_TABLE,
|
||||||
|
Limit = 100,
|
||||||
|
ExclusiveStartKey = lastKey
|
||||||
|
};
|
||||||
|
var response = await _dynamo.ScanAsync(request);
|
||||||
|
foreach (var item in response.Items)
|
||||||
|
yield return ConvertItem(item);
|
||||||
|
lastKey = response.LastEvaluatedKey;
|
||||||
|
} while (lastKey != null && lastKey.Count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string?> ConvertItem(Dictionary<string, AttributeValue> item)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, string?>();
|
||||||
|
foreach (var kvp in item)
|
||||||
|
{
|
||||||
|
if (kvp.Value.S != null)
|
||||||
|
result[kvp.Key] = kvp.Value.S;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
Api.SeaHavenIndustries/Infrastructure/PortAdapters.cs
Normal file
75
Api.SeaHavenIndustries/Infrastructure/PortAdapters.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Infrastructure
|
||||||
|
{
|
||||||
|
public class FileStorageAdapter : IFileStoragePort
|
||||||
|
{
|
||||||
|
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||||
|
private readonly IHttpContextAccessor _httpContext;
|
||||||
|
|
||||||
|
public FileStorageAdapter(IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContext)
|
||||||
|
{
|
||||||
|
_webHostEnvironment = webHostEnvironment;
|
||||||
|
_httpContext = httpContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> SaveFileAsync(IFormFile file)
|
||||||
|
{
|
||||||
|
var fileName = Path.GetFileName(file.FileName);
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName))
|
||||||
|
throw new InvalidDataException("The uploaded file must have a valid name.");
|
||||||
|
|
||||||
|
var uniqueFileName = $"{Guid.NewGuid()}_{fileName}";
|
||||||
|
var uploadPath = Path.Combine("Assets", "Documents");
|
||||||
|
var webRoot = _webHostEnvironment.WebRootPath
|
||||||
|
?? Path.Combine(_webHostEnvironment.ContentRootPath, "wwwroot");
|
||||||
|
var fullPath = Path.Combine(webRoot, uploadPath, uniqueFileName);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
|
||||||
|
|
||||||
|
using (var stream = System.IO.File.Create(fullPath))
|
||||||
|
{
|
||||||
|
await file.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = _httpContext.HttpContext?.Request
|
||||||
|
?? throw new InvalidOperationException("An active HTTP request is required to build the file URL.");
|
||||||
|
var domain = $"{request.Scheme}://{request.Host}";
|
||||||
|
return $"{domain}/{uploadPath.Replace("\\", "/")}/{uniqueFileName}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DispatchEmailAdapter : IDispatchEmailPort
|
||||||
|
{
|
||||||
|
private readonly Helper.SendMessage _sendMessage;
|
||||||
|
|
||||||
|
public DispatchEmailAdapter(Helper.SendMessage sendMessage)
|
||||||
|
{
|
||||||
|
_sendMessage = sendMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SendDispatchEmailAsync(string emailTo, string subject, string htmlBody, string? replyTo)
|
||||||
|
{
|
||||||
|
return await _sendMessage.SendDispatchEmail(emailTo, subject, htmlBody, replyTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VendorTokenAdapter : IVendorTokenPort
|
||||||
|
{
|
||||||
|
private readonly IVendorPortalTokenService _tokenService;
|
||||||
|
|
||||||
|
public VendorTokenAdapter(IVendorPortalTokenService tokenService)
|
||||||
|
{
|
||||||
|
_tokenService = tokenService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetOrCreateActiveTokenAsync(int vendorId)
|
||||||
|
{
|
||||||
|
var token = await _tokenService.GetOrCreateActiveTokenAsync(
|
||||||
|
vendorId, CancellationToken.None);
|
||||||
|
return token.Token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -53,6 +53,10 @@ builder.Services.AddSingleton<SeaHaven.Services.Interfaces.IZipCodeDistance>(
|
||||||
|
|
||||||
builder.Services.AddDataServices();
|
builder.Services.AddDataServices();
|
||||||
builder.Services.AddBusinessServices();
|
builder.Services.AddBusinessServices();
|
||||||
|
builder.Services.AddScoped<SeaHaven.Services.Interfaces.ISyncExternalSource, Api.SeaHavenIndustries.Infrastructure.DynamoSyncExternalSource>();
|
||||||
|
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IFileStoragePort, Api.SeaHavenIndustries.Infrastructure.FileStorageAdapter>();
|
||||||
|
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IDispatchEmailPort, Api.SeaHavenIndustries.Infrastructure.DispatchEmailAdapter>();
|
||||||
|
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IVendorTokenPort, Api.SeaHavenIndustries.Infrastructure.VendorTokenAdapter>();
|
||||||
|
|
||||||
builder.Services.AddAuthentication(options =>
|
builder.Services.AddAuthentication(options =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using Data.SeaHavenIndustries;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SeaHaven.DataServices.Dto;
|
using SeaHaven.DataServices.Dto;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using SeaHaven.DataServices.Models;
|
||||||
|
|
||||||
namespace SeaHaven.DataServices.Implementation
|
namespace SeaHaven.DataServices.Implementation
|
||||||
{
|
{
|
||||||
|
|
@ -26,6 +27,14 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
.FirstOrDefaultAsync(d => d.Id == id);
|
.FirstOrDefaultAsync(d => d.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Dispatch?> GetByIdWithVendorAndWorkOrderAsync(int id)
|
||||||
|
{
|
||||||
|
return await _context.Dispatches
|
||||||
|
.Include(dispatch => dispatch.Vendor)
|
||||||
|
.Include(dispatch => dispatch.WorkOrder)
|
||||||
|
.FirstOrDefaultAsync(dispatch => dispatch.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Dispatch>> GetAllAsync()
|
public async Task<IEnumerable<Dispatch>> GetAllAsync()
|
||||||
{
|
{
|
||||||
return await _context.Dispatches.ToListAsync();
|
return await _context.Dispatches.ToListAsync();
|
||||||
|
|
@ -703,5 +712,90 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
{
|
{
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DispatchDetailReadModel?> GetDispatchDetailAsync(int id)
|
||||||
|
{
|
||||||
|
return await _context.Dispatches
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(dispatch => dispatch.Id == id)
|
||||||
|
.Select(dispatch => new DispatchDetailReadModel
|
||||||
|
{
|
||||||
|
Id = dispatch.Id,
|
||||||
|
DispatchNumber = dispatch.DispatchNumber,
|
||||||
|
PONumber = dispatch.PONumber,
|
||||||
|
NTEAmount = dispatch.NTEAmount,
|
||||||
|
Description = dispatch.Description,
|
||||||
|
Status = dispatch.Status,
|
||||||
|
ScheduledDate = dispatch.ScheduledDate,
|
||||||
|
CompletedDate = dispatch.CompletedDate,
|
||||||
|
DispatchedAt = dispatch.DispatchedAt,
|
||||||
|
EmailSent = dispatch.EmailSent,
|
||||||
|
WorkOrderId = dispatch.WorkOrderId,
|
||||||
|
AcknowledgedAt = dispatch.AcknowledgedAt,
|
||||||
|
VerifiedBy = dispatch.VerifiedBy,
|
||||||
|
VerifiedAt = dispatch.VerifiedAt,
|
||||||
|
WoNumber = dispatch.WorkOrder != null
|
||||||
|
? dispatch.WorkOrder.InternalWONumber
|
||||||
|
: "",
|
||||||
|
WoTitle = dispatch.WorkOrder != null
|
||||||
|
? dispatch.WorkOrder.WorkerOrderTitle
|
||||||
|
: "",
|
||||||
|
VendorName = dispatch.Vendor != null ? dispatch.Vendor.CompanyName : "",
|
||||||
|
VendorEmail = dispatch.Vendor != null ? dispatch.Vendor.Email : "",
|
||||||
|
VendorId = dispatch.VendorId,
|
||||||
|
WorkOrders = _context.DispatchWorkOrders
|
||||||
|
.Where(link => link.DispatchId == dispatch.Id)
|
||||||
|
.Select(link => new DispatchWorkOrderReadModel
|
||||||
|
{
|
||||||
|
WorkOrderId = link.WorkOrderId,
|
||||||
|
WoNumber = link.WorkOrder != null
|
||||||
|
? link.WorkOrder.InternalWONumber
|
||||||
|
: "",
|
||||||
|
WoTitle = link.WorkOrder != null
|
||||||
|
? link.WorkOrder.WorkerOrderTitle
|
||||||
|
: ""
|
||||||
|
}).ToList(),
|
||||||
|
ChecklistItems = _context.DispatchChecklistItems
|
||||||
|
.Where(item => item.DispatchId == dispatch.Id)
|
||||||
|
.OrderBy(item => item.SortOrder)
|
||||||
|
.Select(item => new ChecklistItemReadModel
|
||||||
|
{
|
||||||
|
Id = item.Id,
|
||||||
|
ItemText = item.ItemText,
|
||||||
|
IsCompleted = item.IsCompleted,
|
||||||
|
CompletedBy = item.CompletedBy,
|
||||||
|
CompletedAt = item.CompletedAt,
|
||||||
|
SortOrder = item.SortOrder,
|
||||||
|
WorkOrderId = item.WorkOrderId
|
||||||
|
}).ToList(),
|
||||||
|
Signoffs = _context.DispatchSignoffs
|
||||||
|
.Where(signoff => signoff.DispatchId == dispatch.Id)
|
||||||
|
.Select(signoff => new SignoffReadModel
|
||||||
|
{
|
||||||
|
Id = signoff.Id,
|
||||||
|
SignoffType = signoff.SignoffType,
|
||||||
|
Name = signoff.Name,
|
||||||
|
Signature = signoff.Signature,
|
||||||
|
SignatureMethod = signoff.SignatureMethod,
|
||||||
|
SignedAt = signoff.SignedAt
|
||||||
|
}).ToList(),
|
||||||
|
Comments = _context.Comments
|
||||||
|
.Where(comment => comment.DispatchId == dispatch.Id)
|
||||||
|
.OrderBy(comment => comment.CreatedDate)
|
||||||
|
.Select(comment => new DispatchCommentReadModel
|
||||||
|
{
|
||||||
|
Id = comment.Id,
|
||||||
|
CreatedDate = comment.CreatedDate,
|
||||||
|
Commenttext = comment.Commenttext,
|
||||||
|
Commenter = comment.Commenter,
|
||||||
|
CommentType = comment.CommentType,
|
||||||
|
UserName = comment.ApplicationUser != null
|
||||||
|
? comment.ApplicationUser.FirstName + " "
|
||||||
|
+ comment.ApplicationUser.LastName
|
||||||
|
: ""
|
||||||
|
}).ToList()
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using SeaHaven.DataServices.Models;
|
||||||
|
|
||||||
namespace SeaHaven.DataServices.Implementation
|
namespace SeaHaven.DataServices.Implementation
|
||||||
{
|
{
|
||||||
|
|
@ -71,5 +72,108 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(IReadOnlyList<QuoteListItemReadModel> Items, int TotalCount)> GetQuotesPagedAsync(
|
||||||
|
int page, int pageSize, string search, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var normalizedSearch = search.Trim();
|
||||||
|
var query = _context.Quotes
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(w => normalizedSearch.Length == 0 ||
|
||||||
|
(w.QuotId != null && w.QuotId.Contains(normalizedSearch)) ||
|
||||||
|
(w.SiteName != null && w.SiteName.Contains(normalizedSearch)) ||
|
||||||
|
(w.WorkOrder != null && w.WorkOrder.WorkerOrderTitle != null &&
|
||||||
|
w.WorkOrder.WorkerOrderTitle.Contains(normalizedSearch)));
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
var data = await query
|
||||||
|
.OrderByDescending(s => s.Id)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(s => new QuoteListItemReadModel
|
||||||
|
{
|
||||||
|
Id = s.Id,
|
||||||
|
QuoteId = "QO-" + s.QuotId,
|
||||||
|
WorkOrder = s.WorkOrder != null ? s.WorkOrder.WorkerOrderNumber : "",
|
||||||
|
ProjectName = s.ProjectName,
|
||||||
|
TotalAmount = s.LineItems!.Sum(l => l.Lineitem ?? 0),
|
||||||
|
SiteName = s.SiteName,
|
||||||
|
SiteId = s.SiteId,
|
||||||
|
PO = s.PO,
|
||||||
|
TT = s.TT,
|
||||||
|
BillingAddress = s.BillingAddress,
|
||||||
|
Status = s.Status,
|
||||||
|
ContactId = s.ContactId,
|
||||||
|
Date = s.Date,
|
||||||
|
Lineitems = s.LineItems!.Select(q => new QuoteLineItemReadModel
|
||||||
|
{
|
||||||
|
Descriptions = q.Descriptions,
|
||||||
|
Lineitem = q.Lineitem,
|
||||||
|
LineitemNumber = q.LineitemNumber
|
||||||
|
}).ToList(),
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return (data, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<QuoteDetailReadModel?> GetQuoteDetailsByIdAsync(
|
||||||
|
int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _context.Quotes
|
||||||
|
.Where(s => s.Id == id)
|
||||||
|
.AsNoTracking()
|
||||||
|
.Select(s => new QuoteDetailReadModel
|
||||||
|
{
|
||||||
|
Id = s.Id,
|
||||||
|
QuotId = s.QuotId,
|
||||||
|
Date = s.Date,
|
||||||
|
Name = s.Contacts != null ? (s.Contacts.FirstName + " " + s.Contacts.LastName).Trim() : "",
|
||||||
|
PO = s.PO,
|
||||||
|
TT = s.TT,
|
||||||
|
SiteName = s.SiteName,
|
||||||
|
ProjectName = s.ProjectName,
|
||||||
|
SiteId = s.SiteId,
|
||||||
|
BillingAddress = s.BillingAddress,
|
||||||
|
ShippingAddress = s.ShippingAddress,
|
||||||
|
Lineitems = s.LineItems!.Select(q => new QuoteLineItemReadModel
|
||||||
|
{
|
||||||
|
Descriptions = q.Descriptions,
|
||||||
|
Lineitem = q.Lineitem
|
||||||
|
}).ToList(),
|
||||||
|
TotalAmount = s.LineItems!.Sum(l => l.Lineitem ?? 0)
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<WorkOrderQuoteDetailReadModel>> GetWorkOrderQuoteDetailsAsync(
|
||||||
|
int woId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _context.workOrders
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(w => w.Id == woId)
|
||||||
|
.Select(s => new WorkOrderQuoteDetailReadModel
|
||||||
|
{
|
||||||
|
Id = s.Id,
|
||||||
|
PO = s.PO,
|
||||||
|
TT = s.TT,
|
||||||
|
Title = s.Locations != null ? s.Locations.Title : "",
|
||||||
|
Name = s.Locations != null ? s.Locations.Name : ""
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteLineItemsByQuoteIdAsync(int quoteId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var items = await _context.QuotesLines
|
||||||
|
.Where(w => w.QuoteId == quoteId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (items.Any())
|
||||||
|
{
|
||||||
|
_context.QuotesLines.RemoveRange(items);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
127
SeaHaven.DataServices/Implementation/SyncDataService.cs
Normal file
127
SeaHaven.DataServices/Implementation/SyncDataService.cs
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
|
||||||
|
namespace SeaHaven.DataServices.Implementation
|
||||||
|
{
|
||||||
|
public class SyncDataService : ISyncDataService
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
|
public SyncDataService(ApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetLastInternalWONumberAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _context.workOrders
|
||||||
|
.Where(w => w.InternalWONumber != null && w.InternalWONumber != "")
|
||||||
|
.OrderByDescending(w => w.InternalWONumber)
|
||||||
|
.Select(w => w.InternalWONumber)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WorkOrder?> GetWorkOrderByExternalIdAsync(string externalId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _context.workOrders
|
||||||
|
.FirstOrDefaultAsync(w => w.ExternalWorkOrderId == externalId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnqueueWorkOrder(WorkOrder workOrder)
|
||||||
|
{
|
||||||
|
_context.workOrders.Add(workOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnqueueComment(Comments comment)
|
||||||
|
{
|
||||||
|
_context.Comments.Add(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveChangesAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int?> ResolveLocationAsync(string? siteCode, string? building, string? address, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(siteCode) && string.IsNullOrWhiteSpace(building))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var matchCode = siteCode ?? building;
|
||||||
|
var existing = await _context.Locations
|
||||||
|
.FirstOrDefaultAsync(l => l.Name == matchCode || l.Title == matchCode, cancellationToken);
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
return existing.Id;
|
||||||
|
|
||||||
|
var location = new Locations
|
||||||
|
{
|
||||||
|
Name = matchCode,
|
||||||
|
Title = building,
|
||||||
|
Address1 = address,
|
||||||
|
Status = "Active"
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Locations.Add(location);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return location.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetLocationCountAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _context.Locations.CountAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CommentExistsByExternalIdAsync(string externalId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _context.Comments
|
||||||
|
.AnyAsync(c => c.ExternalCommentId == externalId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WorkOrder?> GetWorkOrderByInternalNumberAsync(string internalNumber, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _context.workOrders
|
||||||
|
.FirstOrDefaultAsync(w => w.InternalWONumber == internalNumber, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dispatch?> GetDispatchByNumberAndWorkOrderIdAsync(string dispatchNumber, int workOrderId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _context.Dispatches
|
||||||
|
.FirstOrDefaultAsync(d => d.DispatchNumber == dispatchNumber && d.WorkOrderId == workOrderId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<WorkOrder>> GetWorkOrdersMissingInternalNumberAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _context.workOrders
|
||||||
|
.Where(w => w.InternalWONumber == null || w.InternalWONumber == "")
|
||||||
|
.OrderBy(w => w.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Dispatch>> GetDispatchesMissingNumberAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _context.Dispatches
|
||||||
|
.Where(d => d.DispatchNumber == null || d.DispatchNumber == "")
|
||||||
|
.OrderBy(d => d.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> ExecuteBackfillCommentTypesAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _context.Database.ExecuteSqlRawAsync(
|
||||||
|
"UPDATE Comments SET CommentType = 'customer' WHERE ExternalCommentId IS NOT NULL AND (CommentType IS NULL OR CommentType = '')",
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetLastDispatchNumberAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _context.Dispatches
|
||||||
|
.Where(d => d.DispatchNumber != null && d.DispatchNumber != "")
|
||||||
|
.OrderByDescending(d => d.DispatchNumber)
|
||||||
|
.Select(d => d.DispatchNumber)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
|
||||||
|
namespace SeaHaven.DataServices.Implementation
|
||||||
|
{
|
||||||
|
public class WorkOrderAuditDataService : IWorkOrderAuditDataService
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
|
public WorkOrderAuditDataService(ApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnqueueAuditLog(WorkOrderAuditLog auditLog)
|
||||||
|
{
|
||||||
|
_context.WorkOrderAuditLogs.Add(auditLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PersistAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex))
|
||||||
|
{
|
||||||
|
foreach (var entry in ex.Entries.Where(e => e.Entity is WorkOrderFieldLock))
|
||||||
|
entry.State = EntityState.Detached;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsUniqueConstraintViolation(DbUpdateException ex)
|
||||||
|
{
|
||||||
|
var message = ex.InnerException?.Message ?? ex.Message;
|
||||||
|
return message.Contains("duplicate", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| message.Contains("UNIQUE", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using SeaHaven.DataServices.Models;
|
||||||
|
|
||||||
namespace SeaHaven.DataServices.Implementation
|
namespace SeaHaven.DataServices.Implementation
|
||||||
{
|
{
|
||||||
|
|
@ -101,7 +102,7 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
return (items, totalCount);
|
return (items, totalCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(IEnumerable<object> Items, int TotalCount)> GetWorkOrderListPagedAsync(
|
public async Task<(IEnumerable<WorkOrderListItemReadModel> Items, int TotalCount)> GetWorkOrderListPagedAsync(
|
||||||
int page,
|
int page,
|
||||||
int pageSize,
|
int pageSize,
|
||||||
string? search = null,
|
string? search = null,
|
||||||
|
|
@ -131,13 +132,12 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
{
|
{
|
||||||
var s = search.ToLower();
|
|
||||||
query = query.Where(w =>
|
query = query.Where(w =>
|
||||||
(w.InternalWONumber ?? "").ToLower().Contains(s) ||
|
(w.InternalWONumber != null && w.InternalWONumber.Contains(search)) ||
|
||||||
(w.WorkerOrderNumber ?? "").ToLower().Contains(s) ||
|
(w.WorkerOrderNumber != null && w.WorkerOrderNumber.Contains(search)) ||
|
||||||
(w.WorkerOrderTitle ?? "").ToLower().Contains(s) ||
|
(w.WorkerOrderTitle != null && w.WorkerOrderTitle.Contains(search)) ||
|
||||||
(w.Locations.Title ?? "").ToLower().Contains(s) ||
|
(w.Locations != null && w.Locations.Title != null && w.Locations.Title.Contains(search)) ||
|
||||||
(w.Locations.Name ?? "").ToLower().Contains(s)
|
(w.Locations != null && w.Locations.Name != null && w.Locations.Name.Contains(search))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,24 +160,24 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
var data = await ordered
|
var data = await ordered
|
||||||
.Skip((page - 1) * pageSize)
|
.Skip((page - 1) * pageSize)
|
||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
.Select(w => new
|
.Select(w => new WorkOrderListItemReadModel
|
||||||
{
|
{
|
||||||
id = w.Id,
|
Id = w.Id,
|
||||||
internalWONumber = w.InternalWONumber,
|
InternalWONumber = w.InternalWONumber,
|
||||||
workOrderNumber = w.WorkerOrderNumber,
|
WorkOrderNumber = w.WorkerOrderNumber,
|
||||||
number = w.InternalWONumber ?? w.WorkerOrderNumber,
|
Number = w.InternalWONumber ?? w.WorkerOrderNumber,
|
||||||
title = w.WorkerOrderTitle,
|
Title = w.WorkerOrderTitle,
|
||||||
location = w.Locations != null ? w.Locations.Name : "",
|
Location = w.Locations != null ? w.Locations.Name : "",
|
||||||
locationName = w.Locations != null ? w.Locations.Name : "",
|
LocationName = w.Locations != null ? w.Locations.Name : "",
|
||||||
priority = w.Priority,
|
Priority = w.Priority,
|
||||||
status = w.Status,
|
Status = w.Status,
|
||||||
createdDate = w.CreatedDate,
|
CreatedDate = w.CreatedDate,
|
||||||
dueDate = w.DueDate,
|
DueDate = w.DueDate,
|
||||||
assignedTo = w.AssignToUser != null ? w.AssignToUser.FirstName + " " + w.AssignToUser.LastName : "",
|
AssignedTo = w.AssignToUser != null ? w.AssignToUser.FirstName + " " + w.AssignToUser.LastName : "",
|
||||||
employeeName = w.AssignToUser != null ? w.AssignToUser.FirstName + " " + w.AssignToUser.LastName : "",
|
EmployeeName = w.AssignToUser != null ? w.AssignToUser.FirstName + " " + w.AssignToUser.LastName : "",
|
||||||
lastUpdated = new[] {
|
LastUpdated = new[] {
|
||||||
w.LastModificationTime,
|
w.LastModificationTime,
|
||||||
w.Comments.Max(c => (DateTime?)c.CreatedDate),
|
w.Comments!.Max(c => (DateTime?)c.CreatedDate),
|
||||||
_context.WorkOrderAuditLogs.Where(a => a.WorkOrderId == w.Id).Max(a => (DateTime?)a.CreatedAt)
|
_context.WorkOrderAuditLogs.Where(a => a.WorkOrderId == w.Id).Max(a => (DateTime?)a.CreatedAt)
|
||||||
}.Where(d => d.HasValue).Max()
|
}.Where(d => d.HasValue).Max()
|
||||||
})
|
})
|
||||||
|
|
@ -349,6 +349,11 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
return await _context.workOrders.CountAsync(w => w.Status == status);
|
return await _context.workOrders.CountAsync(w => w.Status == status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> CountByPriorityAsync(string priority)
|
||||||
|
{
|
||||||
|
return await _context.workOrders.CountAsync(w => w.Priority == priority);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<int> CountByLocationAsync(int locationId)
|
public async Task<int> CountByLocationAsync(int locationId)
|
||||||
{
|
{
|
||||||
return await _context.workOrders.CountAsync(w => w.LocationId == locationId);
|
return await _context.workOrders.CountAsync(w => w.LocationId == locationId);
|
||||||
|
|
@ -363,5 +368,121 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id)
|
||||||
|
{
|
||||||
|
return await _context.workOrders
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(w => w.Id == id)
|
||||||
|
.Select(s => new WorkOrderDetailReadModel
|
||||||
|
{
|
||||||
|
Id = s.Id,
|
||||||
|
InternalWONumber = s.InternalWONumber,
|
||||||
|
WorkerOrderNumber = s.WorkerOrderNumber,
|
||||||
|
Title = s.WorkerOrderTitle,
|
||||||
|
Description = s.Description,
|
||||||
|
CreatedDate = s.CreatedDate,
|
||||||
|
DueDate = s.DueDate,
|
||||||
|
ScheduledDate = s.ScheduledDate,
|
||||||
|
CompletedDate = s.CompletedDate,
|
||||||
|
Status = s.Status,
|
||||||
|
Assignee = s.AssignToUser != null ? s.AssignToUser.FirstName : "",
|
||||||
|
Location = s.Locations != null ? s.Locations.Name : "",
|
||||||
|
LocationId = s.LocationId,
|
||||||
|
LocationZip = s.Locations != null ? (s.Locations.Zip ?? ExtractZip(s.Locations.Address1)) : "",
|
||||||
|
Priority = s.Priority,
|
||||||
|
Problem = s.Problem,
|
||||||
|
Trade = s.Trade,
|
||||||
|
SubTrade = s.SubTrade,
|
||||||
|
VendorNTE = s.VendorNTE,
|
||||||
|
Source = s.Source,
|
||||||
|
PO = s.PO,
|
||||||
|
TT = s.TT,
|
||||||
|
Poc = s.WorkOrderContacts!.Select(c => new ContactReadModel
|
||||||
|
{
|
||||||
|
Id = c.Id,
|
||||||
|
Name = c.POC != null ? (c.POC.FirstName + " " + c.POC.LastName).Trim() : "",
|
||||||
|
Email = c.POC != null ? c.POC.Email : "",
|
||||||
|
Phone = c.POC != null ? c.POC.PhoneNumber : ""
|
||||||
|
}).ToList(),
|
||||||
|
Category = s.WorkOrderCategories!.Select(c => new CategoryReadModel
|
||||||
|
{
|
||||||
|
Id = c.Id,
|
||||||
|
Name = c.Category != null ? c.Category.Name : ""
|
||||||
|
}).ToList(),
|
||||||
|
Attachments = s.workOrderAttachments!.Select(a => new AttachmentReadModel
|
||||||
|
{
|
||||||
|
Id = a.Id,
|
||||||
|
Attachment = a.Attachments
|
||||||
|
}).ToList(),
|
||||||
|
BeforPhotoIssue = s.BeforPhotoIssue,
|
||||||
|
BeforPhotoAttachment = s.BeforPhotoAttachment,
|
||||||
|
AfterPhotoIssue = s.AfterPhotoIssue,
|
||||||
|
AfterPhotoAttachment = s.AfterPhotoAttachment,
|
||||||
|
SignOffName = s.SignOffName,
|
||||||
|
SignOffAttachment = s.SignOffAttachment,
|
||||||
|
SignOffSignature = s.SignOffSignature,
|
||||||
|
Comments = s.Comments!.Select(c => new CommentReadModel
|
||||||
|
{
|
||||||
|
Id = c.Id,
|
||||||
|
CreatedDate = c.CreatedDate,
|
||||||
|
Commenttext = c.Commenttext,
|
||||||
|
Commenter = c.Commenter,
|
||||||
|
RecordType = c.RecordType,
|
||||||
|
CommentType = c.CommentType,
|
||||||
|
Documents = c.Documents,
|
||||||
|
UserName = c.ApplicationUser != null ? c.ApplicationUser.FirstName : ""
|
||||||
|
}).ToList(),
|
||||||
|
Quotes = s.Quotes!.Select(q => new QuoteRefReadModel
|
||||||
|
{
|
||||||
|
Code = q.QuotId,
|
||||||
|
Id = q.Id
|
||||||
|
}).ToList(),
|
||||||
|
AuditLog = _context.WorkOrderAuditLogs
|
||||||
|
.Where(a => a.WorkOrderId == s.Id)
|
||||||
|
.OrderByDescending(a => a.CreatedAt)
|
||||||
|
.Select(a => new AuditLogReadModel
|
||||||
|
{
|
||||||
|
Id = a.Id,
|
||||||
|
FieldName = a.FieldName,
|
||||||
|
OldValue = a.OldValue,
|
||||||
|
NewValue = a.NewValue,
|
||||||
|
Action = a.Action,
|
||||||
|
CreatedAt = a.CreatedAt,
|
||||||
|
UserName = a.User != null ? a.User.FirstName + " " + a.User.LastName : ""
|
||||||
|
}).ToList(),
|
||||||
|
Dispatches = s.Dispatches!.Select(d => new DispatchReadModel
|
||||||
|
{
|
||||||
|
Id = d.Id,
|
||||||
|
DispatchNumber = d.DispatchNumber,
|
||||||
|
PONumber = d.PONumber,
|
||||||
|
NTEAmount = d.NTEAmount,
|
||||||
|
Description = d.Description,
|
||||||
|
Status = d.Status,
|
||||||
|
ScheduledDate = d.ScheduledDate,
|
||||||
|
CompletedDate = d.CompletedDate,
|
||||||
|
DispatchedAt = d.DispatchedAt,
|
||||||
|
EmailSent = d.EmailSent,
|
||||||
|
VendorName = d.Vendor != null ? d.Vendor.CompanyName : "",
|
||||||
|
VendorEmail = d.Vendor != null ? d.Vendor.Email : "",
|
||||||
|
VendorId = d.VendorId
|
||||||
|
}).ToList()
|
||||||
|
}).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<WorkOrder>> GetNonTemplateWorkOrdersWithLocationsAsync()
|
||||||
|
{
|
||||||
|
return await _context.workOrders
|
||||||
|
.Where(w => w.istemplate != true)
|
||||||
|
.Include(w => w.Locations)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractZip(string? address)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(address)) return null;
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(address, @"\b(\d{5})(?:-\d{4})?\b");
|
||||||
|
return match.Success ? match.Groups[1].Value : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
|
||||||
|
namespace SeaHaven.DataServices.Implementation
|
||||||
|
{
|
||||||
|
public class WorkOrderFieldLockDataService : IWorkOrderFieldLockDataService
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
|
public WorkOrderFieldLockDataService(ApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsLockedAsync(int workOrderId, string fieldName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (IsTracked(workOrderId, fieldName))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return await _context.WorkOrderFieldLocks
|
||||||
|
.AnyAsync(l => l.WorkOrderId == workOrderId && l.FieldName == fieldName, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnqueueLock(int workOrderId, string fieldName, string? userId)
|
||||||
|
{
|
||||||
|
if (IsTracked(workOrderId, fieldName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_context.WorkOrderFieldLocks.Add(new WorkOrderFieldLock
|
||||||
|
{
|
||||||
|
WorkOrderId = workOrderId,
|
||||||
|
FieldName = fieldName,
|
||||||
|
LockedAt = DateTime.UtcNow,
|
||||||
|
LockedByUserId = userId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsTracked(int workOrderId, string fieldName)
|
||||||
|
=> _context.WorkOrderFieldLocks.Local.Any(l =>
|
||||||
|
l.WorkOrderId == workOrderId &&
|
||||||
|
string.Equals(l.FieldName, fieldName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using SeaHaven.DataServices.Dto;
|
using SeaHaven.DataServices.Dto;
|
||||||
|
using SeaHaven.DataServices.Models;
|
||||||
|
|
||||||
namespace SeaHaven.DataServices.Interfaces
|
namespace SeaHaven.DataServices.Interfaces
|
||||||
{
|
{
|
||||||
|
|
@ -7,6 +8,7 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
{
|
{
|
||||||
Task<Dispatch?> GetByIdAsync(int id);
|
Task<Dispatch?> GetByIdAsync(int id);
|
||||||
Task<Dispatch?> GetByIdWithDetailsAsync(int id);
|
Task<Dispatch?> GetByIdWithDetailsAsync(int id);
|
||||||
|
Task<Dispatch?> GetByIdWithVendorAndWorkOrderAsync(int id);
|
||||||
Task<IEnumerable<Dispatch>> GetAllAsync();
|
Task<IEnumerable<Dispatch>> GetAllAsync();
|
||||||
Task<IEnumerable<Dispatch>> GetByVendorIdAsync(int vendorId);
|
Task<IEnumerable<Dispatch>> GetByVendorIdAsync(int vendorId);
|
||||||
Task<IEnumerable<Dispatch>> GetByWorkOrderIdAsync(int workOrderId);
|
Task<IEnumerable<Dispatch>> GetByWorkOrderIdAsync(int workOrderId);
|
||||||
|
|
@ -33,6 +35,7 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
|
|
||||||
Task<DispatchListResult> GetListAsync(DispatchListQuery query, CancellationToken cancellationToken);
|
Task<DispatchListResult> GetListAsync(DispatchListQuery query, CancellationToken cancellationToken);
|
||||||
Task<DispatchDetailDTO?> GetDetailAsync(int id, CancellationToken cancellationToken);
|
Task<DispatchDetailDTO?> GetDetailAsync(int id, CancellationToken cancellationToken);
|
||||||
|
Task<DispatchDetailReadModel?> GetDispatchDetailAsync(int id);
|
||||||
Task<IReadOnlyList<VendorDispatchSummaryData>> GetVendorDispatchSummariesAsync(
|
Task<IReadOnlyList<VendorDispatchSummaryData>> GetVendorDispatchSummariesAsync(
|
||||||
int vendorId, string? status, CancellationToken cancellationToken);
|
int vendorId, string? status, CancellationToken cancellationToken);
|
||||||
Task<Dispatch?> GetVendorDispatchDetailAsync(
|
Task<Dispatch?> GetVendorDispatchDetailAsync(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
|
using SeaHaven.DataServices.Models;
|
||||||
|
|
||||||
namespace SeaHaven.DataServices.Interfaces
|
namespace SeaHaven.DataServices.Interfaces
|
||||||
{
|
{
|
||||||
|
|
@ -12,5 +13,15 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
Task DeleteAsync(int id);
|
Task DeleteAsync(int id);
|
||||||
Task<bool> ExistsAsync(int id);
|
Task<bool> ExistsAsync(int id);
|
||||||
Task UnlinkFromWorkOrderAsync(int workOrderId);
|
Task UnlinkFromWorkOrderAsync(int workOrderId);
|
||||||
|
|
||||||
|
Task<(IReadOnlyList<QuoteListItemReadModel> Items, int TotalCount)> GetQuotesPagedAsync(
|
||||||
|
int page, int pageSize, string search, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<QuoteDetailReadModel?> GetQuoteDetailsByIdAsync(int id, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<WorkOrderQuoteDetailReadModel>> GetWorkOrderQuoteDetailsAsync(
|
||||||
|
int woId, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task DeleteLineItemsByQuoteIdAsync(int quoteId, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
SeaHaven.DataServices/Interfaces/ISyncDataService.cs
Normal file
25
SeaHaven.DataServices/Interfaces/ISyncDataService.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
|
||||||
|
namespace SeaHaven.DataServices.Interfaces
|
||||||
|
{
|
||||||
|
public interface ISyncDataService
|
||||||
|
{
|
||||||
|
Task<string?> GetLastInternalWONumberAsync(CancellationToken cancellationToken);
|
||||||
|
Task<WorkOrder?> GetWorkOrderByExternalIdAsync(string externalId, CancellationToken cancellationToken);
|
||||||
|
void EnqueueWorkOrder(WorkOrder workOrder);
|
||||||
|
void EnqueueComment(Comments comment);
|
||||||
|
Task SaveChangesAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<int?> ResolveLocationAsync(string? siteCode, string? building, string? address, CancellationToken cancellationToken);
|
||||||
|
Task<int> GetLocationCountAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<bool> CommentExistsByExternalIdAsync(string externalId, CancellationToken cancellationToken);
|
||||||
|
Task<WorkOrder?> GetWorkOrderByInternalNumberAsync(string internalNumber, CancellationToken cancellationToken);
|
||||||
|
Task<Dispatch?> GetDispatchByNumberAndWorkOrderIdAsync(string dispatchNumber, int workOrderId, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<List<WorkOrder>> GetWorkOrdersMissingInternalNumberAsync(CancellationToken cancellationToken);
|
||||||
|
Task<List<Dispatch>> GetDispatchesMissingNumberAsync(CancellationToken cancellationToken);
|
||||||
|
Task<int> ExecuteBackfillCommentTypesAsync(CancellationToken cancellationToken);
|
||||||
|
Task<string?> GetLastDispatchNumberAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
|
||||||
|
namespace SeaHaven.DataServices.Interfaces
|
||||||
|
{
|
||||||
|
public interface IWorkOrderAuditDataService
|
||||||
|
{
|
||||||
|
void EnqueueAuditLog(WorkOrderAuditLog auditLog);
|
||||||
|
|
||||||
|
Task PersistAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
|
using SeaHaven.DataServices.Models;
|
||||||
|
|
||||||
namespace SeaHaven.DataServices.Interfaces
|
namespace SeaHaven.DataServices.Interfaces
|
||||||
{
|
{
|
||||||
|
|
@ -23,7 +24,7 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
string? priority = null);
|
string? priority = null);
|
||||||
|
|
||||||
// Paged list with detailed information (for GetWorkOrderList endpoint)
|
// Paged list with detailed information (for GetWorkOrderList endpoint)
|
||||||
Task<(IEnumerable<object> Items, int TotalCount)> GetWorkOrderListPagedAsync(
|
Task<(IEnumerable<WorkOrderListItemReadModel> Items, int TotalCount)> GetWorkOrderListPagedAsync(
|
||||||
int page,
|
int page,
|
||||||
int pageSize,
|
int pageSize,
|
||||||
string? search = null,
|
string? search = null,
|
||||||
|
|
@ -66,7 +67,12 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
// Statistics
|
// Statistics
|
||||||
Task<int> CountAsync();
|
Task<int> CountAsync();
|
||||||
Task<int> CountByStatusAsync(string status);
|
Task<int> CountByStatusAsync(string status);
|
||||||
|
Task<int> CountByPriorityAsync(string priority);
|
||||||
Task<int> CountByLocationAsync(int locationId);
|
Task<int> CountByLocationAsync(int locationId);
|
||||||
Task<IEnumerable<WorkOrder>> GetAllWithDetailsAsync();
|
Task<IEnumerable<WorkOrder>> GetAllWithDetailsAsync();
|
||||||
|
|
||||||
|
// Complex read models
|
||||||
|
Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id);
|
||||||
|
Task<List<WorkOrder>> GetNonTemplateWorkOrdersWithLocationsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
|
||||||
|
namespace SeaHaven.DataServices.Interfaces
|
||||||
|
{
|
||||||
|
public interface IWorkOrderFieldLockDataService
|
||||||
|
{
|
||||||
|
Task<bool> IsLockedAsync(int workOrderId, string fieldName, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
void EnqueueLock(int workOrderId, string fieldName, string? userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
260
SeaHaven.DataServices/Models/ReadModels.cs
Normal file
260
SeaHaven.DataServices/Models/ReadModels.cs
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
namespace SeaHaven.DataServices.Models
|
||||||
|
{
|
||||||
|
public class WorkOrderListItemReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? InternalWONumber { get; set; }
|
||||||
|
public string? WorkOrderNumber { get; set; }
|
||||||
|
public string? Number { get; set; }
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? Location { get; set; }
|
||||||
|
public string? LocationName { get; set; }
|
||||||
|
public string? Priority { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public DateTime? CreatedDate { get; set; }
|
||||||
|
public DateTime? DueDate { get; set; }
|
||||||
|
public string? AssignedTo { get; set; }
|
||||||
|
public string? EmployeeName { get; set; }
|
||||||
|
public DateTime? LastUpdated { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkOrderDetailReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? InternalWONumber { get; set; }
|
||||||
|
public string? WorkerOrderNumber { get; set; }
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public DateTime? CreatedDate { get; set; }
|
||||||
|
public DateTime? DueDate { get; set; }
|
||||||
|
public DateTime? ScheduledDate { get; set; }
|
||||||
|
public DateTime? CompletedDate { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public string? Assignee { get; set; }
|
||||||
|
public string? Location { get; set; }
|
||||||
|
public int? LocationId { get; set; }
|
||||||
|
public string? LocationZip { get; set; }
|
||||||
|
public string? Priority { get; set; }
|
||||||
|
public string? Problem { get; set; }
|
||||||
|
public string? Trade { get; set; }
|
||||||
|
public string? SubTrade { get; set; }
|
||||||
|
public decimal? VendorNTE { get; set; }
|
||||||
|
public string? Source { get; set; }
|
||||||
|
public string? PO { get; set; }
|
||||||
|
public string? TT { get; set; }
|
||||||
|
public List<ContactReadModel> Poc { get; set; } = new();
|
||||||
|
public List<CategoryReadModel> Category { get; set; } = new();
|
||||||
|
public List<AttachmentReadModel> Attachments { get; set; } = new();
|
||||||
|
public string? BeforPhotoIssue { get; set; }
|
||||||
|
public string? BeforPhotoAttachment { get; set; }
|
||||||
|
public string? AfterPhotoIssue { get; set; }
|
||||||
|
public string? AfterPhotoAttachment { get; set; }
|
||||||
|
public string? SignOffName { get; set; }
|
||||||
|
public string? SignOffAttachment { get; set; }
|
||||||
|
public string? SignOffSignature { get; set; }
|
||||||
|
public List<CommentReadModel> Comments { get; set; } = new();
|
||||||
|
public List<QuoteRefReadModel> Quotes { get; set; } = new();
|
||||||
|
public List<AuditLogReadModel> AuditLog { get; set; } = new();
|
||||||
|
public List<DispatchReadModel> Dispatches { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ContactReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CategoryReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AttachmentReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? Attachment { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CommentReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public DateTime? CreatedDate { get; set; }
|
||||||
|
public string? Commenttext { get; set; }
|
||||||
|
public string? Commenter { get; set; }
|
||||||
|
public string? RecordType { get; set; }
|
||||||
|
public string? CommentType { get; set; }
|
||||||
|
public string? Documents { get; set; }
|
||||||
|
public string? UserName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuoteRefReadModel
|
||||||
|
{
|
||||||
|
public string? Code { get; set; }
|
||||||
|
public int Id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AuditLogReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? FieldName { get; set; }
|
||||||
|
public string? OldValue { get; set; }
|
||||||
|
public string? NewValue { get; set; }
|
||||||
|
public string? Action { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public string? UserName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DispatchReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? DispatchNumber { get; set; }
|
||||||
|
public string? PONumber { get; set; }
|
||||||
|
public decimal? NTEAmount { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public DateTime? ScheduledDate { get; set; }
|
||||||
|
public DateTime? CompletedDate { get; set; }
|
||||||
|
public DateTime? DispatchedAt { get; set; }
|
||||||
|
public bool? EmailSent { get; set; }
|
||||||
|
public string? VendorName { get; set; }
|
||||||
|
public string? VendorEmail { get; set; }
|
||||||
|
public int? VendorId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DispatchDetailReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? DispatchNumber { get; set; }
|
||||||
|
public string? PONumber { get; set; }
|
||||||
|
public decimal? NTEAmount { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public DateTime? ScheduledDate { get; set; }
|
||||||
|
public DateTime? CompletedDate { get; set; }
|
||||||
|
public DateTime? DispatchedAt { get; set; }
|
||||||
|
public bool? EmailSent { get; set; }
|
||||||
|
public int? WorkOrderId { get; set; }
|
||||||
|
public DateTime? AcknowledgedAt { get; set; }
|
||||||
|
public string? VerifiedBy { get; set; }
|
||||||
|
public DateTime? VerifiedAt { get; set; }
|
||||||
|
public string? WoNumber { get; set; }
|
||||||
|
public string? WoTitle { get; set; }
|
||||||
|
public string? VendorName { get; set; }
|
||||||
|
public string? VendorEmail { get; set; }
|
||||||
|
public int? VendorId { get; set; }
|
||||||
|
public List<DispatchWorkOrderReadModel> WorkOrders { get; set; } = new();
|
||||||
|
public List<ChecklistItemReadModel> ChecklistItems { get; set; } = new();
|
||||||
|
public List<SignoffReadModel> Signoffs { get; set; } = new();
|
||||||
|
public List<DispatchCommentReadModel> Comments { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DispatchWorkOrderReadModel
|
||||||
|
{
|
||||||
|
public int? WorkOrderId { get; set; }
|
||||||
|
public string? WoNumber { get; set; }
|
||||||
|
public string? WoTitle { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChecklistItemReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? ItemText { get; set; }
|
||||||
|
public bool? IsCompleted { get; set; }
|
||||||
|
public string? CompletedBy { get; set; }
|
||||||
|
public DateTime? CompletedAt { get; set; }
|
||||||
|
public int? SortOrder { get; set; }
|
||||||
|
public int? WorkOrderId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SignoffReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? SignoffType { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Signature { get; set; }
|
||||||
|
public string? SignatureMethod { get; set; }
|
||||||
|
public DateTime? SignedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DispatchCommentReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public DateTime? CreatedDate { get; set; }
|
||||||
|
public string? Commenttext { get; set; }
|
||||||
|
public string? Commenter { get; set; }
|
||||||
|
public string? CommentType { get; set; }
|
||||||
|
public string? UserName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FilteredWorkOrderReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? WorkerOrderTitle { get; set; }
|
||||||
|
public DateTime? DueDate { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public string? Priority { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CommentListItemReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? Commenttext { get; set; }
|
||||||
|
public string? FirstName { get; set; }
|
||||||
|
public string? Documents { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuoteListItemReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? QuoteId { get; set; }
|
||||||
|
public string? WorkOrder { get; set; }
|
||||||
|
public string? ProjectName { get; set; }
|
||||||
|
public decimal TotalAmount { get; set; }
|
||||||
|
public string? SiteName { get; set; }
|
||||||
|
public string? SiteId { get; set; }
|
||||||
|
public string? PO { get; set; }
|
||||||
|
public string? TT { get; set; }
|
||||||
|
public string? BillingAddress { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public int? ContactId { get; set; }
|
||||||
|
public DateTime? Date { get; set; }
|
||||||
|
public List<QuoteLineItemReadModel> Lineitems { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuoteDetailReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? QuotId { get; set; }
|
||||||
|
public DateTime? Date { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? PO { get; set; }
|
||||||
|
public string? TT { get; set; }
|
||||||
|
public string? SiteName { get; set; }
|
||||||
|
public string? ProjectName { get; set; }
|
||||||
|
public string? SiteId { get; set; }
|
||||||
|
public string? BillingAddress { get; set; }
|
||||||
|
public string? ShippingAddress { get; set; }
|
||||||
|
public decimal TotalAmount { get; set; }
|
||||||
|
public List<QuoteLineItemReadModel> Lineitems { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuoteLineItemReadModel
|
||||||
|
{
|
||||||
|
public string? Descriptions { get; set; }
|
||||||
|
public decimal? Lineitem { get; set; }
|
||||||
|
public int? LineitemNumber { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkOrderQuoteDetailReadModel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? PO { get; set; }
|
||||||
|
public string? TT { get; set; }
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
131
SeaHaven.Services/DTOs/DispatchServiceDTOs.cs
Normal file
131
SeaHaven.Services/DTOs/DispatchServiceDTOs.cs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
namespace SeaHaven.Services.DTOs
|
||||||
|
{
|
||||||
|
public class DispatchVerificationException : Exception
|
||||||
|
{
|
||||||
|
public List<string> Missing { get; }
|
||||||
|
public DispatchVerificationException(List<string> missing) : base("Cannot verify")
|
||||||
|
{
|
||||||
|
Missing = missing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DispatchToVendorInput
|
||||||
|
{
|
||||||
|
public List<int> WorkOrderIds { get; set; } = new();
|
||||||
|
public List<int> VendorIds { get; set; } = new();
|
||||||
|
public decimal? NTEAmount { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public DateTime? ScheduledDate { get; set; }
|
||||||
|
public int? TaskListTemplateId { get; set; }
|
||||||
|
public List<string>? CustomChecklistItems { get; set; }
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
public string? SenderName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DispatchResultItem
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? DispatchNumber { get; set; }
|
||||||
|
public string? PONumber { get; set; }
|
||||||
|
public string? VendorName { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public int WorkOrderCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DispatchToVendorResult
|
||||||
|
{
|
||||||
|
public string Message { get; set; } = "";
|
||||||
|
public List<DispatchResultItem> Dispatches { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateDispatchInput
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public decimal? NTEAmount { get; set; }
|
||||||
|
public DateTime? ScheduledDate { get; set; }
|
||||||
|
public DateTime? CompletedDate { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AddDispatchCommentInput
|
||||||
|
{
|
||||||
|
public int DispatchId { get; set; }
|
||||||
|
public string? Text { get; set; }
|
||||||
|
public bool SendEmail { get; set; }
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AddDispatchCommentResult
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public DateTime? CreatedDate { get; set; }
|
||||||
|
public string? Commenttext { get; set; }
|
||||||
|
public string? CommentType { get; set; }
|
||||||
|
public string? UserName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AddDispatchSignoffInput
|
||||||
|
{
|
||||||
|
public int DispatchId { get; set; }
|
||||||
|
public string? SignoffType { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Signature { get; set; }
|
||||||
|
public string? SignatureMethod { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AddDispatchSignoffResult
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? SignoffType { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public DateTime? SignedAt { get; set; }
|
||||||
|
public string? SignatureMethod { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VerifyDispatchResult
|
||||||
|
{
|
||||||
|
public string Message { get; set; } = "";
|
||||||
|
public string? VerifiedBy { get; set; }
|
||||||
|
public DateTime? VerifiedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VerifyDispatchError
|
||||||
|
{
|
||||||
|
public string Status { get; set; } = "Error";
|
||||||
|
public string Message { get; set; } = "";
|
||||||
|
public List<string> Missing { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChecklistItemUpdateInput
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public bool IsCompleted { get; set; }
|
||||||
|
public string? CompletedBy { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChecklistItemUpdateResult
|
||||||
|
{
|
||||||
|
public string Message { get; set; } = "";
|
||||||
|
public int Id { get; set; }
|
||||||
|
public bool? IsCompleted { get; set; }
|
||||||
|
public string? CompletedBy { get; set; }
|
||||||
|
public DateTime? CompletedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AddChecklistItemInput
|
||||||
|
{
|
||||||
|
public int DispatchId { get; set; }
|
||||||
|
public int? WorkOrderId { get; set; }
|
||||||
|
public string? ItemText { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AddChecklistItemResult
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? ItemText { get; set; }
|
||||||
|
public int? SortOrder { get; set; }
|
||||||
|
public bool? IsCompleted { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
108
SeaHaven.Services/DTOs/QuoteDTOs.cs
Normal file
108
SeaHaven.Services/DTOs/QuoteDTOs.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
namespace SeaHaven.Services.DTOs
|
||||||
|
{
|
||||||
|
public class CreateQuoteInput
|
||||||
|
{
|
||||||
|
public string? QuotId { get; set; }
|
||||||
|
public int? ContactId { get; set; }
|
||||||
|
public string? PO { get; set; }
|
||||||
|
public string? TT { get; set; }
|
||||||
|
public string? ProjectName { get; set; }
|
||||||
|
public DateTime? Date { get; set; }
|
||||||
|
public int? WorkorderId { get; set; }
|
||||||
|
public string? SiteName { get; set; }
|
||||||
|
public string? SiteId { get; set; }
|
||||||
|
public string? BillingAddress { get; set; }
|
||||||
|
public List<QuoteLineItemInput>? LineItems { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateQuoteInput
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? QuotId { get; set; }
|
||||||
|
public int? ContactId { get; set; }
|
||||||
|
public string? PO { get; set; }
|
||||||
|
public string? TT { get; set; }
|
||||||
|
public string? ProjectName { get; set; }
|
||||||
|
public DateTime? Date { get; set; }
|
||||||
|
public int? WorkorderId { get; set; }
|
||||||
|
public string? SiteName { get; set; }
|
||||||
|
public string? SiteId { get; set; }
|
||||||
|
public string? BillingAddress { get; set; }
|
||||||
|
public List<QuoteLineItemInput>? LineItems { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuoteLineItemInput
|
||||||
|
{
|
||||||
|
public int? LineitemNumber { get; set; }
|
||||||
|
public decimal? Lineitem { get; set; }
|
||||||
|
public string? Descriptions { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PagedQuotesResult
|
||||||
|
{
|
||||||
|
public IReadOnlyList<QuoteListItemDTO> Items { get; set; } = Array.Empty<QuoteListItemDTO>();
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuoteListItemDTO
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? QuoteId { get; set; }
|
||||||
|
public string? WorkOrder { get; set; }
|
||||||
|
public string? ProjectName { get; set; }
|
||||||
|
public string? TotalAmount { get; set; }
|
||||||
|
public string? SiteName { get; set; }
|
||||||
|
public string? SiteId { get; set; }
|
||||||
|
public string? PO { get; set; }
|
||||||
|
public string? TT { get; set; }
|
||||||
|
public string? BillingAddress { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public int? ContactId { get; set; }
|
||||||
|
public DateTime? Date { get; set; }
|
||||||
|
public IReadOnlyList<QuoteLineItemDTO> Lineitems { get; set; } = Array.Empty<QuoteLineItemDTO>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuoteDetailDTO
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? QuotId { get; set; }
|
||||||
|
public DateTime? Date { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? PO { get; set; }
|
||||||
|
public string? TT { get; set; }
|
||||||
|
public string? SiteName { get; set; }
|
||||||
|
public string? ProjectName { get; set; }
|
||||||
|
public string? SiteId { get; set; }
|
||||||
|
public string? BillingAddress { get; set; }
|
||||||
|
public string? ShippingAddress { get; set; }
|
||||||
|
public string? TotalAmount { get; set; }
|
||||||
|
public IReadOnlyList<QuoteLineItemDTO> Lineitems { get; set; } = Array.Empty<QuoteLineItemDTO>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuoteLineItemDTO
|
||||||
|
{
|
||||||
|
public string? Descriptions { get; set; }
|
||||||
|
public decimal? Lineitem { get; set; }
|
||||||
|
public int? LineitemNumber { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkOrderQuoteDetailDTO
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? PO { get; set; }
|
||||||
|
public string? TT { get; set; }
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuoteReportingData
|
||||||
|
{
|
||||||
|
public int TotalCreatedWorkOrder { get; set; }
|
||||||
|
public int TotalCompleted { get; set; }
|
||||||
|
public int TotalSev1 { get; set; }
|
||||||
|
public int TotalSev2 { get; set; }
|
||||||
|
public int TotalSev3 { get; set; }
|
||||||
|
public int TotalSev4 { get; set; }
|
||||||
|
public int TotalSev5 { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
28
SeaHaven.Services/DTOs/SyncDTOs.cs
Normal file
28
SeaHaven.Services/DTOs/SyncDTOs.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
namespace SeaHaven.Services.DTOs
|
||||||
|
{
|
||||||
|
public class SyncWorkOrdersResult
|
||||||
|
{
|
||||||
|
public int Synced { get; set; }
|
||||||
|
public int Created { get; set; }
|
||||||
|
public int Updated { get; set; }
|
||||||
|
public int LocationsCreated { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SyncCommentsResult
|
||||||
|
{
|
||||||
|
public int Synced { get; set; }
|
||||||
|
public int Created { get; set; }
|
||||||
|
public int Skipped { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BackfillResult
|
||||||
|
{
|
||||||
|
public int Updated { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SyncAllResult
|
||||||
|
{
|
||||||
|
public SyncWorkOrdersResult? WorkOrders { get; set; }
|
||||||
|
public SyncCommentsResult? Comments { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
115
SeaHaven.Services/DTOs/WorkOrderServiceDTOs.cs
Normal file
115
SeaHaven.Services/DTOs/WorkOrderServiceDTOs.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using SeaHaven.DataServices.Models;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.DTOs
|
||||||
|
{
|
||||||
|
public class CreateWorkOrderWithDetailsInput
|
||||||
|
{
|
||||||
|
public string WorkerOrderTitle { get; set; } = "";
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string Status { get; set; } = "Open";
|
||||||
|
public string? Priority { get; set; }
|
||||||
|
public int? LocationId { get; set; }
|
||||||
|
public string? AssignTo { get; set; }
|
||||||
|
public DateTime? DueDate { get; set; }
|
||||||
|
public List<int>? ContactIds { get; set; }
|
||||||
|
public List<int>? CategoryIds { get; set; }
|
||||||
|
public List<IFormFile>? Attachments { get; set; }
|
||||||
|
public IFormFile? BeforPhotoAttachment { get; set; }
|
||||||
|
public IFormFile? AfterPhotoAttachment { get; set; }
|
||||||
|
public IFormFile? SignOffAttachment { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateWorkOrderWithDetailsInput
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? WorkerOrderTitle { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public string? Priority { get; set; }
|
||||||
|
public int? LocationId { get; set; }
|
||||||
|
public string? AssignTo { get; set; }
|
||||||
|
public DateTime? DueDate { get; set; }
|
||||||
|
public DateTime? CompletedDate { get; set; }
|
||||||
|
public List<int>? ContactIds { get; set; }
|
||||||
|
public List<int>? CategoryIds { get; set; }
|
||||||
|
public List<IFormFile>? Attachments { get; set; }
|
||||||
|
public IFormFile? BeforPhotoAttachment { get; set; }
|
||||||
|
public IFormFile? AfterPhotoAttachment { get; set; }
|
||||||
|
public IFormFile? SignOffAttachment { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AddCommentInput
|
||||||
|
{
|
||||||
|
public int WorkorderId { get; set; }
|
||||||
|
public string? Text { get; set; }
|
||||||
|
public string? CommentType { get; set; }
|
||||||
|
public IFormFile? Document { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CommentResult
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public DateTime? CreatedDate { get; set; }
|
||||||
|
public string? Commenttext { get; set; }
|
||||||
|
public string? CommentType { get; set; }
|
||||||
|
public string? UserName { get; set; }
|
||||||
|
public string? Documents { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChangeStatusResult
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? InternalWONumber { get; set; }
|
||||||
|
public string? WorkerOrderNumber { get; set; }
|
||||||
|
public string? WorkerOrderTitle { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public string? Priority { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChangeAssignmentResult
|
||||||
|
{
|
||||||
|
public string Message { get; set; } = "";
|
||||||
|
public string AssignedTo { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkorderDDItem
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? InternalWONumber { get; set; }
|
||||||
|
public string? WorkerOrderNumber { get; set; }
|
||||||
|
public string? WorkerOrderTitle { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public string? Priority { get; set; }
|
||||||
|
public bool istemplate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkorderFilterVM
|
||||||
|
{
|
||||||
|
public int? Id { get; set; }
|
||||||
|
public int? LocationId { get; set; }
|
||||||
|
public string? WorkerOrderNumber { get; set; }
|
||||||
|
public string? BillingAddress { get; set; } = "Amazon.com Services LLC 410 Terry Ave,North Seattle, Washington, 98109";
|
||||||
|
public string? WorkerOrderTitle { get; set; }
|
||||||
|
public string? LocationName { get; set; }
|
||||||
|
public string? AssignToUserName { get; set; }
|
||||||
|
public string? Priority { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public DateTime? DueDate { get; set; }
|
||||||
|
public string FormattedDueDate => DueDate.HasValue ? DueDate.Value.ToString("MM/dd/yyyy") : "";
|
||||||
|
public string? Dispatcher { get; set; }
|
||||||
|
public string? Scheduler { get; set; }
|
||||||
|
public string? PO { get; set; }
|
||||||
|
public string? TT { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FilteredWorkOrderPageDTO
|
||||||
|
{
|
||||||
|
public IReadOnlyList<FilteredWorkOrderReadModel> Data { get; set; } =
|
||||||
|
Array.Empty<FilteredWorkOrderReadModel>();
|
||||||
|
public int PageNumber { get; set; }
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
public int TotalPages { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
191
SeaHaven.Services/Implementation/QuotesService.cs
Normal file
191
SeaHaven.Services/Implementation/QuotesService.cs
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Implementation
|
||||||
|
{
|
||||||
|
public class QuotesService : IQuotesService
|
||||||
|
{
|
||||||
|
private readonly IQuotesDataService _quotesData;
|
||||||
|
private readonly IWorkOrderDataService _workOrderData;
|
||||||
|
|
||||||
|
public QuotesService(IQuotesDataService quotesData, IWorkOrderDataService workOrderData)
|
||||||
|
{
|
||||||
|
_quotesData = quotesData;
|
||||||
|
_workOrderData = workOrderData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateQuoteAsync(CreateQuoteInput input, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var rnd = new Random();
|
||||||
|
var id = rnd.Next(1, 99999);
|
||||||
|
|
||||||
|
var quote = new Quotes
|
||||||
|
{
|
||||||
|
QuotId = id.ToString(),
|
||||||
|
ContactId = input.ContactId,
|
||||||
|
PO = input.PO,
|
||||||
|
TT = input.TT,
|
||||||
|
ProjectName = input.ProjectName,
|
||||||
|
Date = input.Date,
|
||||||
|
WorkorderId = input.WorkorderId,
|
||||||
|
SiteName = input.SiteName,
|
||||||
|
SiteId = input.SiteId,
|
||||||
|
BillingAddress = input.BillingAddress,
|
||||||
|
LineItems = new List<QuotesLineItems>()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (input.LineItems != null && input.LineItems.Any())
|
||||||
|
{
|
||||||
|
foreach (var li in input.LineItems)
|
||||||
|
{
|
||||||
|
quote.LineItems.Add(new QuotesLineItems
|
||||||
|
{
|
||||||
|
LineitemNumber = li.LineitemNumber,
|
||||||
|
Lineitem = li.Lineitem,
|
||||||
|
Descriptions = li.Descriptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _quotesData.AddAsync(quote);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateQuoteAsync(UpdateQuoteInput input, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var quote = await _quotesData.GetByIdAsync(input.Id);
|
||||||
|
if (quote == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
quote.QuotId = input.QuotId;
|
||||||
|
quote.ContactId = input.ContactId;
|
||||||
|
quote.PO = input.PO;
|
||||||
|
quote.TT = input.TT;
|
||||||
|
quote.ProjectName = input.ProjectName;
|
||||||
|
quote.Date = input.Date;
|
||||||
|
quote.WorkorderId = input.WorkorderId;
|
||||||
|
quote.SiteName = input.SiteName;
|
||||||
|
quote.SiteId = input.SiteId;
|
||||||
|
quote.BillingAddress = input.BillingAddress;
|
||||||
|
|
||||||
|
await _quotesData.DeleteLineItemsByQuoteIdAsync(input.Id, cancellationToken);
|
||||||
|
|
||||||
|
quote.LineItems ??= new List<QuotesLineItems>();
|
||||||
|
|
||||||
|
if (input.LineItems != null && input.LineItems.Any())
|
||||||
|
{
|
||||||
|
foreach (var li in input.LineItems)
|
||||||
|
{
|
||||||
|
quote.LineItems.Add(new QuotesLineItems
|
||||||
|
{
|
||||||
|
LineitemNumber = li.LineitemNumber,
|
||||||
|
Lineitem = li.Lineitem,
|
||||||
|
Descriptions = li.Descriptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _quotesData.UpdateAsync(quote);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteQuoteAsync(int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var quote = await _quotesData.GetByIdAsync(id);
|
||||||
|
if (quote == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await _quotesData.DeleteLineItemsByQuoteIdAsync(id, cancellationToken);
|
||||||
|
await _quotesData.DeleteAsync(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedQuotesResult> GetQuotesPagedAsync(int page, int pageSize, string search, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var (items, totalCount) = await _quotesData.GetQuotesPagedAsync(page, pageSize, search, cancellationToken);
|
||||||
|
|
||||||
|
return new PagedQuotesResult
|
||||||
|
{
|
||||||
|
Items = items.Select(item => new QuoteListItemDTO
|
||||||
|
{
|
||||||
|
Id = item.Id,
|
||||||
|
QuoteId = item.QuoteId,
|
||||||
|
WorkOrder = item.WorkOrder,
|
||||||
|
ProjectName = item.ProjectName,
|
||||||
|
TotalAmount = FormatAmount(item.TotalAmount),
|
||||||
|
SiteName = item.SiteName,
|
||||||
|
SiteId = item.SiteId,
|
||||||
|
PO = item.PO,
|
||||||
|
TT = item.TT,
|
||||||
|
BillingAddress = item.BillingAddress,
|
||||||
|
Status = item.Status,
|
||||||
|
ContactId = item.ContactId,
|
||||||
|
Date = item.Date,
|
||||||
|
Lineitems = item.Lineitems.Select(MapLineItem).ToList()
|
||||||
|
}).ToList(),
|
||||||
|
TotalCount = totalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<WorkOrderQuoteDetailDTO>> GetWorkOrderQuoteDetailsAsync(
|
||||||
|
int woId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var items = await _quotesData.GetWorkOrderQuoteDetailsAsync(woId, cancellationToken);
|
||||||
|
return items.Select(item => new WorkOrderQuoteDetailDTO
|
||||||
|
{
|
||||||
|
Id = item.Id,
|
||||||
|
PO = item.PO,
|
||||||
|
TT = item.TT,
|
||||||
|
Title = item.Title,
|
||||||
|
Name = item.Name
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<QuoteDetailDTO?> GetQuoteDetailsByIdAsync(
|
||||||
|
int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var item = await _quotesData.GetQuoteDetailsByIdAsync(id, cancellationToken);
|
||||||
|
return item == null ? null : new QuoteDetailDTO
|
||||||
|
{
|
||||||
|
Id = item.Id,
|
||||||
|
QuotId = item.QuotId,
|
||||||
|
Date = item.Date,
|
||||||
|
Name = item.Name,
|
||||||
|
PO = item.PO,
|
||||||
|
TT = item.TT,
|
||||||
|
SiteName = item.SiteName,
|
||||||
|
ProjectName = item.ProjectName,
|
||||||
|
SiteId = item.SiteId,
|
||||||
|
BillingAddress = item.BillingAddress,
|
||||||
|
ShippingAddress = item.ShippingAddress,
|
||||||
|
TotalAmount = FormatAmount(item.TotalAmount),
|
||||||
|
Lineitems = item.Lineitems.Select(MapLineItem).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<QuoteReportingData> GetReportingAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return new QuoteReportingData
|
||||||
|
{
|
||||||
|
TotalCreatedWorkOrder = await _workOrderData.CountAsync(),
|
||||||
|
TotalCompleted = await _workOrderData.CountByStatusAsync("Done"),
|
||||||
|
TotalSev1 = await _workOrderData.CountByPriorityAsync("Sev 1"),
|
||||||
|
TotalSev2 = await _workOrderData.CountByPriorityAsync("Sev 2"),
|
||||||
|
TotalSev3 = await _workOrderData.CountByPriorityAsync("Sev 3"),
|
||||||
|
TotalSev4 = await _workOrderData.CountByPriorityAsync("Sev 4"),
|
||||||
|
TotalSev5 = await _workOrderData.CountByPriorityAsync("Sev 5")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static QuoteLineItemDTO MapLineItem(
|
||||||
|
SeaHaven.DataServices.Models.QuoteLineItemReadModel item) => new()
|
||||||
|
{
|
||||||
|
Descriptions = item.Descriptions,
|
||||||
|
Lineitem = item.Lineitem,
|
||||||
|
LineitemNumber = item.LineitemNumber
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string FormatAmount(decimal amount) => $"${amount}";
|
||||||
|
}
|
||||||
|
}
|
||||||
304
SeaHaven.Services/Implementation/SyncService.cs
Normal file
304
SeaHaven.Services/Implementation/SyncService.cs
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Implementation
|
||||||
|
{
|
||||||
|
public class SyncService : ISyncService
|
||||||
|
{
|
||||||
|
private readonly ISyncDataService _data;
|
||||||
|
private readonly ISyncExternalSource _external;
|
||||||
|
|
||||||
|
public SyncService(ISyncDataService data, ISyncExternalSource external)
|
||||||
|
{
|
||||||
|
_data = data;
|
||||||
|
_external = external;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SyncWorkOrdersResult> SyncWorkOrdersAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var synced = 0;
|
||||||
|
var created = 0;
|
||||||
|
var updated = 0;
|
||||||
|
|
||||||
|
var lastInternalWO = await _data.GetLastInternalWONumberAsync(cancellationToken);
|
||||||
|
int nextInternal = 10000001;
|
||||||
|
if (lastInternalWO != null && int.TryParse(lastInternalWO, out var parsed))
|
||||||
|
nextInternal = parsed + 1;
|
||||||
|
|
||||||
|
await foreach (var item in _external.ScanWorkOrdersAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
var externalId = Get(item, "work_order_id");
|
||||||
|
if (string.IsNullOrEmpty(externalId)) continue;
|
||||||
|
|
||||||
|
var existing = await _data.GetWorkOrderByExternalIdAsync(externalId, cancellationToken);
|
||||||
|
|
||||||
|
var locationId = await _data.ResolveLocationAsync(
|
||||||
|
Get(item, "site_code"),
|
||||||
|
Get(item, "building"),
|
||||||
|
Get(item, "address"),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
var wo = new WorkOrder
|
||||||
|
{
|
||||||
|
InternalWONumber = (nextInternal++).ToString("D8"),
|
||||||
|
ExternalWorkOrderId = externalId,
|
||||||
|
WorkerOrderNumber = externalId,
|
||||||
|
WorkerOrderTitle = Get(item, "description"),
|
||||||
|
Description = Get(item, "description"),
|
||||||
|
Status = MapStatus(Get(item, "wo_status")),
|
||||||
|
Priority = MapSeverityToPriority(Get(item, "severity")),
|
||||||
|
Severity = Get(item, "severity"),
|
||||||
|
Customer = Get(item, "customer"),
|
||||||
|
SiteCode = Get(item, "site_code"),
|
||||||
|
Building = Get(item, "building"),
|
||||||
|
LocationId = locationId,
|
||||||
|
DueDate = ParseDate(Get(item, "due_date")),
|
||||||
|
DateReported = ParseDate(Get(item, "date_reported")),
|
||||||
|
ScheduledStart = ParseDate(Get(item, "scheduled_start")),
|
||||||
|
AssignTo = null,
|
||||||
|
SourceEmailS3Key = Get(item, "source_email_s3_key"),
|
||||||
|
CreatedDate = ParseDate(Get(item, "created_at")) ?? DateTime.UtcNow,
|
||||||
|
istemplate = false
|
||||||
|
};
|
||||||
|
_data.EnqueueWorkOrder(wo);
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.WorkerOrderTitle = Get(item, "description") ?? existing.WorkerOrderTitle;
|
||||||
|
existing.Description = Get(item, "description") ?? existing.Description;
|
||||||
|
existing.Status = MapStatus(Get(item, "wo_status")) ?? existing.Status;
|
||||||
|
existing.Priority = MapSeverityToPriority(Get(item, "severity")) ?? existing.Priority;
|
||||||
|
existing.Severity = Get(item, "severity") ?? existing.Severity;
|
||||||
|
existing.SiteCode = Get(item, "site_code") ?? existing.SiteCode;
|
||||||
|
existing.Building = Get(item, "building") ?? existing.Building;
|
||||||
|
existing.LocationId = locationId ?? existing.LocationId;
|
||||||
|
existing.DueDate = ParseDate(Get(item, "due_date")) ?? existing.DueDate;
|
||||||
|
existing.DateReported = ParseDate(Get(item, "date_reported")) ?? existing.DateReported;
|
||||||
|
existing.ScheduledStart = ParseDate(Get(item, "scheduled_start")) ?? existing.ScheduledStart;
|
||||||
|
existing.SourceEmailS3Key = Get(item, "source_email_s3_key") ?? existing.SourceEmailS3Key;
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
synced++;
|
||||||
|
|
||||||
|
if (synced % 100 == 0)
|
||||||
|
await _data.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _data.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new SyncWorkOrdersResult
|
||||||
|
{
|
||||||
|
Synced = synced,
|
||||||
|
Created = created,
|
||||||
|
Updated = updated,
|
||||||
|
LocationsCreated = await _data.GetLocationCountAsync(cancellationToken)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SyncCommentsResult> SyncCommentsAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var synced = 0;
|
||||||
|
var created = 0;
|
||||||
|
var skipped = 0;
|
||||||
|
|
||||||
|
await foreach (var item in _external.ScanCommentsAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
var externalCommentId = Get(item, "comment_id");
|
||||||
|
var externalWoId = Get(item, "work_order_id");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(externalCommentId) || string.IsNullOrEmpty(externalWoId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var alreadyExists = await _data.CommentExistsByExternalIdAsync(externalCommentId, cancellationToken);
|
||||||
|
|
||||||
|
if (alreadyExists)
|
||||||
|
{
|
||||||
|
skipped++;
|
||||||
|
synced++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var workOrder = await _data.GetWorkOrderByExternalIdAsync(externalWoId, cancellationToken);
|
||||||
|
|
||||||
|
if (workOrder == null)
|
||||||
|
{
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var comment = new Comments
|
||||||
|
{
|
||||||
|
ExternalCommentId = externalCommentId,
|
||||||
|
WorkerOrderId = workOrder.Id,
|
||||||
|
Commenttext = Get(item, "text"),
|
||||||
|
Commenter = Get(item, "commenter"),
|
||||||
|
RecordType = Get(item, "record_type"),
|
||||||
|
CommentType = "customer",
|
||||||
|
CreatedDate = ParseDate(Get(item, "created_at")) ?? DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
_data.EnqueueComment(comment);
|
||||||
|
created++;
|
||||||
|
synced++;
|
||||||
|
|
||||||
|
if (synced % 100 == 0)
|
||||||
|
await _data.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _data.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new SyncCommentsResult { Synced = synced, Created = created, Skipped = skipped };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BackfillResult> BackfillInternalWONumbersAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var wosMissing = await _data.GetWorkOrdersMissingInternalNumberAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (wosMissing.Count == 0)
|
||||||
|
return new BackfillResult { Updated = 0 };
|
||||||
|
|
||||||
|
var lastWO = await _data.GetLastInternalWONumberAsync(cancellationToken);
|
||||||
|
|
||||||
|
int next = 10000001;
|
||||||
|
if (lastWO != null && int.TryParse(lastWO, out var lastNum))
|
||||||
|
next = lastNum + 1;
|
||||||
|
|
||||||
|
foreach (var wo in wosMissing)
|
||||||
|
{
|
||||||
|
wo.InternalWONumber = next.ToString("D8");
|
||||||
|
next++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _data.SaveChangesAsync(cancellationToken);
|
||||||
|
return new BackfillResult { Updated = wosMissing.Count };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BackfillResult> BackfillDispatchNumbersAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var missing = await _data.GetDispatchesMissingNumberAsync(cancellationToken);
|
||||||
|
if (missing.Count == 0) return new BackfillResult { Updated = 0 };
|
||||||
|
|
||||||
|
int next = 1;
|
||||||
|
var last = await _data.GetLastDispatchNumberAsync(cancellationToken);
|
||||||
|
if (last != null && last.StartsWith("DSP-") && int.TryParse(last.Substring(4), out var n))
|
||||||
|
next = n + 1;
|
||||||
|
|
||||||
|
foreach (var d in missing)
|
||||||
|
{
|
||||||
|
d.DispatchNumber = $"DSP-{next:D5}";
|
||||||
|
next++;
|
||||||
|
}
|
||||||
|
await _data.SaveChangesAsync(cancellationToken);
|
||||||
|
return new BackfillResult { Updated = missing.Count };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SyncCommentsResult> SyncVendorRepliesAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var synced = 0;
|
||||||
|
var created = 0;
|
||||||
|
var skipped = 0;
|
||||||
|
|
||||||
|
await foreach (var item in _external.ScanVendorRepliesAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
var replyId = Get(item, "reply_id");
|
||||||
|
var woNumber = Get(item, "internal_wo_number");
|
||||||
|
var dispatchNumber = Get(item, "dispatch_number");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(replyId) || string.IsNullOrEmpty(woNumber))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var alreadyExists = await _data.CommentExistsByExternalIdAsync(replyId, cancellationToken);
|
||||||
|
if (alreadyExists) { skipped++; synced++; continue; }
|
||||||
|
|
||||||
|
var workOrder = await _data.GetWorkOrderByInternalNumberAsync(woNumber, cancellationToken);
|
||||||
|
if (workOrder == null) { skipped++; continue; }
|
||||||
|
|
||||||
|
int? dispatchId = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(dispatchNumber))
|
||||||
|
{
|
||||||
|
var dispatch = await _data.GetDispatchByNumberAndWorkOrderIdAsync(dispatchNumber, workOrder.Id, cancellationToken);
|
||||||
|
dispatchId = dispatch?.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var comment = new Comments
|
||||||
|
{
|
||||||
|
ExternalCommentId = replyId,
|
||||||
|
WorkerOrderId = workOrder.Id,
|
||||||
|
DispatchId = dispatchId,
|
||||||
|
Commenttext = Get(item, "reply_text"),
|
||||||
|
Commenter = Get(item, "sender_email"),
|
||||||
|
CommentType = "vendor",
|
||||||
|
RecordType = "vendor_reply",
|
||||||
|
CreatedDate = ParseDate(Get(item, "received_at")) ?? DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
_data.EnqueueComment(comment);
|
||||||
|
created++;
|
||||||
|
synced++;
|
||||||
|
|
||||||
|
if (synced % 50 == 0)
|
||||||
|
await _data.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _data.SaveChangesAsync(cancellationToken);
|
||||||
|
return new SyncCommentsResult { Synced = synced, Created = created, Skipped = skipped };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BackfillResult> BackfillCommentTypesAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var updated = await _data.ExecuteBackfillCommentTypesAsync(cancellationToken);
|
||||||
|
return new BackfillResult { Updated = updated };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SyncAllResult> SyncAllAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var woResult = await SyncWorkOrdersAsync(cancellationToken);
|
||||||
|
var commentResult = await SyncCommentsAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new SyncAllResult
|
||||||
|
{
|
||||||
|
WorkOrders = woResult,
|
||||||
|
Comments = commentResult
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Get(Dictionary<string, string?> item, string key)
|
||||||
|
{
|
||||||
|
return item.TryGetValue(key, out var val) ? val : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime? ParseDate(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||||
|
if (DateTime.TryParse(value, out var dt)) return dt;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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",
|
||||||
|
_ => "Open"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? MapSeverityToPriority(string? severity)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(severity)) return null;
|
||||||
|
return $"Sev {severity}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Data.SeaHavenIndustries.Enums;
|
using Data.SeaHavenIndustries.Enums;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
using SeaHaven.Services.Constants;
|
using SeaHaven.Services.Constants;
|
||||||
using SeaHaven.Services.Interfaces;
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
|
|
@ -8,12 +8,12 @@ namespace SeaHaven.Services.Implementation
|
||||||
{
|
{
|
||||||
public class WorkOrderAuditService : IWorkOrderAuditService
|
public class WorkOrderAuditService : IWorkOrderAuditService
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly IWorkOrderAuditDataService _auditData;
|
||||||
private readonly IWorkOrderFieldLockService _fieldLocks;
|
private readonly IWorkOrderFieldLockService _fieldLocks;
|
||||||
|
|
||||||
public WorkOrderAuditService(ApplicationDbContext context, IWorkOrderFieldLockService fieldLocks)
|
public WorkOrderAuditService(IWorkOrderAuditDataService auditData, IWorkOrderFieldLockService fieldLocks)
|
||||||
{
|
{
|
||||||
_context = context;
|
_auditData = auditData;
|
||||||
_fieldLocks = fieldLocks;
|
_fieldLocks = fieldLocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
int? dispatchId = null,
|
int? dispatchId = null,
|
||||||
string? correlationId = null)
|
string? correlationId = null)
|
||||||
{
|
{
|
||||||
_context.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
_auditData.EnqueueAuditLog(new WorkOrderAuditLog
|
||||||
{
|
{
|
||||||
WorkOrderId = workOrderId,
|
WorkOrderId = workOrderId,
|
||||||
UserId = actorId,
|
UserId = actorId,
|
||||||
|
|
@ -64,29 +64,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
await _fieldLocks.LockFieldAsync(workOrderId, fieldName, actorId);
|
await _fieldLocks.LockFieldAsync(workOrderId, fieldName, actorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await SaveChangesIdempotentAsync();
|
await _auditData.PersistAsync(CancellationToken.None);
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveChangesIdempotentAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex))
|
|
||||||
{
|
|
||||||
foreach (var entry in ex.Entries.Where(e => e.Entity is WorkOrderFieldLock))
|
|
||||||
entry.State = EntityState.Detached;
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsUniqueConstraintViolation(DbUpdateException ex)
|
|
||||||
{
|
|
||||||
var message = ex.InnerException?.Message ?? ex.Message;
|
|
||||||
return message.Contains("duplicate", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| message.Contains("UNIQUE", StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
411
SeaHaven.Services/Implementation/WorkOrderDispatchService.cs
Normal file
411
SeaHaven.Services/Implementation/WorkOrderDispatchService.cs
Normal file
|
|
@ -0,0 +1,411 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using SeaHaven.DataServices.Models;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Implementation
|
||||||
|
{
|
||||||
|
public class WorkOrderDispatchService : IWorkOrderDispatchService
|
||||||
|
{
|
||||||
|
private readonly IWorkOrderDataService _workOrderDataService;
|
||||||
|
private readonly IDispatchDataService _dispatchDataService;
|
||||||
|
private readonly IVendorDataService _vendorDataService;
|
||||||
|
private readonly IUserDataService _userDataService;
|
||||||
|
private readonly ITaskListTemplateDataService _taskListTemplateDataService;
|
||||||
|
private readonly ICommentDataService _commentDataService;
|
||||||
|
private readonly IDispatchEmailPort _emailPort;
|
||||||
|
private readonly IVendorTokenPort _vendorTokenPort;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public WorkOrderDispatchService(
|
||||||
|
IWorkOrderDataService workOrderDataService,
|
||||||
|
IDispatchDataService dispatchDataService,
|
||||||
|
IVendorDataService vendorDataService,
|
||||||
|
IUserDataService userDataService,
|
||||||
|
ITaskListTemplateDataService taskListTemplateDataService,
|
||||||
|
ICommentDataService commentDataService,
|
||||||
|
IDispatchEmailPort emailPort,
|
||||||
|
IVendorTokenPort vendorTokenPort,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_workOrderDataService = workOrderDataService;
|
||||||
|
_dispatchDataService = dispatchDataService;
|
||||||
|
_vendorDataService = vendorDataService;
|
||||||
|
_userDataService = userDataService;
|
||||||
|
_taskListTemplateDataService = taskListTemplateDataService;
|
||||||
|
_commentDataService = commentDataService;
|
||||||
|
_emailPort = emailPort;
|
||||||
|
_vendorTokenPort = vendorTokenPort;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DispatchToVendorResult> DispatchToVendorAsync(DispatchToVendorInput input)
|
||||||
|
{
|
||||||
|
var allWorkOrders = await _workOrderDataService.GetAllWithDetailsAsync();
|
||||||
|
var workOrders = allWorkOrders.Where(w => input.WorkOrderIds.Contains(w.Id)).ToList();
|
||||||
|
|
||||||
|
if (workOrders.Count == 0)
|
||||||
|
throw new InvalidOperationException("No work orders found");
|
||||||
|
|
||||||
|
var primaryWO = workOrders.First();
|
||||||
|
|
||||||
|
var lastPO = await _dispatchDataService.GenerateNextPONumberAsync();
|
||||||
|
int nextPO = int.TryParse(lastPO, out var po) ? po : 1;
|
||||||
|
|
||||||
|
var lastDSP = await _dispatchDataService.GenerateNextDispatchNumberAsync();
|
||||||
|
int nextDSP = lastDSP.StartsWith("DSP-") && int.TryParse(lastDSP.Substring(4), out var dspNum) ? dspNum : 1;
|
||||||
|
|
||||||
|
var currentUser = input.UserId != null ? await _userDataService.GetByIdAsync(input.UserId) : null;
|
||||||
|
var senderName = currentUser != null ? (currentUser.FirstName + " " + currentUser.LastName).Trim() : "";
|
||||||
|
|
||||||
|
var dispatches = new List<DispatchResultItem>();
|
||||||
|
|
||||||
|
foreach (var vendorId in input.VendorIds)
|
||||||
|
{
|
||||||
|
var vendor = await _vendorDataService.GetByIdAsync(vendorId);
|
||||||
|
if (vendor == null) continue;
|
||||||
|
|
||||||
|
var poNumber = $"VPO-{nextPO:D5}";
|
||||||
|
nextPO++;
|
||||||
|
var dispatchNumber = $"DSP-{nextDSP:D5}";
|
||||||
|
nextDSP++;
|
||||||
|
|
||||||
|
var replyTo = $"wo-{primaryWO.InternalWONumber}-{dispatchNumber}@int.seahaven.com";
|
||||||
|
|
||||||
|
var dispatch = new Dispatch
|
||||||
|
{
|
||||||
|
WorkOrderId = primaryWO.Id,
|
||||||
|
VendorId = vendorId,
|
||||||
|
DispatchNumber = dispatchNumber,
|
||||||
|
PONumber = poNumber,
|
||||||
|
NTEAmount = input.NTEAmount,
|
||||||
|
Description = input.Description,
|
||||||
|
Status = "Sent",
|
||||||
|
ScheduledDate = input.ScheduledDate,
|
||||||
|
DispatchedAt = DateTime.UtcNow,
|
||||||
|
ReplyToAddress = replyTo,
|
||||||
|
DispatchWorkOrders = workOrders.Select(wo => new DispatchWorkOrder { WorkOrderId = wo.Id }).ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
var checklistItems = new List<DispatchChecklistItem>();
|
||||||
|
if (input.TaskListTemplateId.HasValue)
|
||||||
|
{
|
||||||
|
var template = await _taskListTemplateDataService.GetByIdWithItemsAsync(input.TaskListTemplateId.Value);
|
||||||
|
if (template?.Items != null)
|
||||||
|
{
|
||||||
|
checklistItems.AddRange(template.Items.OrderBy(i => i.SortOrder).Select((i, idx) =>
|
||||||
|
new DispatchChecklistItem { ItemText = i.ItemText, SortOrder = idx }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (input.CustomChecklistItems != null)
|
||||||
|
{
|
||||||
|
var startIdx = checklistItems.Count;
|
||||||
|
checklistItems.AddRange(input.CustomChecklistItems.Select((text, idx) =>
|
||||||
|
new DispatchChecklistItem { ItemText = text, SortOrder = startIdx + idx }));
|
||||||
|
}
|
||||||
|
dispatch.ChecklistItems = checklistItems;
|
||||||
|
|
||||||
|
var woListHtml = string.Join("", workOrders.Select(wo =>
|
||||||
|
$"<tr><td style='padding:6px 8px;border:1px solid #ddd;'>{System.Net.WebUtility.HtmlEncode(wo.InternalWONumber ?? "")}</td>" +
|
||||||
|
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{System.Net.WebUtility.HtmlEncode(wo.WorkerOrderTitle ?? "")}</td>" +
|
||||||
|
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{System.Net.WebUtility.HtmlEncode(wo.Priority ?? "")}</td>" +
|
||||||
|
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{wo.DueDate?.ToString("yyyy-MM-dd") ?? "N/A"}</td></tr>"));
|
||||||
|
|
||||||
|
var locationName = System.Net.WebUtility.HtmlEncode(primaryWO.Locations?.Name ?? "");
|
||||||
|
var locationAddr = System.Net.WebUtility.HtmlEncode(primaryWO.Locations?.Address1 ?? "");
|
||||||
|
var subject = $"[{dispatchNumber}] {(workOrders.Count > 1 ? $"{workOrders.Count} Work Orders" : primaryWO.WorkerOrderTitle)} at {primaryWO.Locations?.Name ?? ""}";
|
||||||
|
var descriptionHtml = System.Net.WebUtility.HtmlEncode(input.Description ?? primaryWO.Description ?? "");
|
||||||
|
var senderNameHtml = System.Net.WebUtility.HtmlEncode(senderName);
|
||||||
|
|
||||||
|
var vendorToken = await _vendorTokenPort.GetOrCreateActiveTokenAsync(vendorId);
|
||||||
|
var frontendBase = _configuration.GetValue<string>("FrontendBaseUrl")?.TrimEnd('/') ?? "";
|
||||||
|
var portalUrl = $"{frontendBase}/v/{vendorToken}/dashboard";
|
||||||
|
|
||||||
|
var html = $@"
|
||||||
|
<h2>Work Order Dispatch — {dispatchNumber}</h2>
|
||||||
|
<p><strong>Location:</strong> {locationName} {locationAddr}</p>
|
||||||
|
<p><strong>Vendor PO:</strong> {poNumber} <strong>NTE:</strong> ${input.NTEAmount?.ToString("F2") ?? "N/A"}</p>
|
||||||
|
<h3>Work Orders</h3>
|
||||||
|
<table style='border-collapse:collapse;width:100%;font-family:Arial,sans-serif;'>
|
||||||
|
<tr style='background:#f5f5f5;'>
|
||||||
|
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>WO #</th>
|
||||||
|
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Title</th>
|
||||||
|
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Priority</th>
|
||||||
|
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Due Date</th>
|
||||||
|
</tr>
|
||||||
|
{woListHtml}
|
||||||
|
</table>
|
||||||
|
<h3>Description</h3>
|
||||||
|
<p>{descriptionHtml}</p>
|
||||||
|
<br/>
|
||||||
|
<div style='text-align:center;margin:20px 0;'>
|
||||||
|
<a href='{portalUrl}' style='display:inline-block;padding:12px 32px;background:#28a745;color:white;text-decoration:none;border-radius:6px;font-weight:bold;font-size:16px;'>View Work Order</a>
|
||||||
|
</div>
|
||||||
|
<br/><p style='color:#666;font-size:13px;'>{senderNameHtml}<br/>Sea Haven Industries</p>
|
||||||
|
<hr/><p style='color:#999;font-size:12px;'>Reply to this email to communicate about this dispatch.</p>";
|
||||||
|
|
||||||
|
var emailSent = await _emailPort.SendDispatchEmailAsync(vendor.Email!, subject, html, replyTo);
|
||||||
|
dispatch.EmailSent = emailSent;
|
||||||
|
|
||||||
|
var savedDispatch = await _dispatchDataService.AddAsync(dispatch);
|
||||||
|
|
||||||
|
foreach (var wo in workOrders)
|
||||||
|
{
|
||||||
|
await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog
|
||||||
|
{
|
||||||
|
WorkOrderId = wo.Id,
|
||||||
|
UserId = input.UserId,
|
||||||
|
FieldName = "Dispatch",
|
||||||
|
OldValue = "",
|
||||||
|
NewValue = $"{vendor.CompanyName} ({dispatchNumber})",
|
||||||
|
Action = AuditAction.Dispatch.ToString(),
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatches.Add(new DispatchResultItem
|
||||||
|
{
|
||||||
|
Id = savedDispatch.Id,
|
||||||
|
DispatchNumber = dispatchNumber,
|
||||||
|
PONumber = poNumber,
|
||||||
|
VendorName = vendor.CompanyName,
|
||||||
|
Status = savedDispatch.Status,
|
||||||
|
WorkOrderCount = workOrders.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DispatchToVendorResult { Message = "Dispatched", Dispatches = dispatches };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<DispatchReadModel>> GetDispatchesAsync(int workOrderId)
|
||||||
|
{
|
||||||
|
var dispatches = await _dispatchDataService.GetByWorkOrderIdAsync(workOrderId);
|
||||||
|
return dispatches.OrderByDescending(d => d.DispatchedAt).Select(d => new DispatchReadModel
|
||||||
|
{
|
||||||
|
Id = d.Id,
|
||||||
|
DispatchNumber = d.DispatchNumber,
|
||||||
|
PONumber = d.PONumber,
|
||||||
|
NTEAmount = d.NTEAmount,
|
||||||
|
Description = d.Description,
|
||||||
|
Status = d.Status,
|
||||||
|
ScheduledDate = d.ScheduledDate,
|
||||||
|
CompletedDate = d.CompletedDate,
|
||||||
|
DispatchedAt = d.DispatchedAt,
|
||||||
|
EmailSent = d.EmailSent,
|
||||||
|
VendorName = d.Vendor != null ? d.Vendor.CompanyName : "",
|
||||||
|
VendorEmail = d.Vendor != null ? d.Vendor.Email : "",
|
||||||
|
VendorId = d.VendorId
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DispatchDetailReadModel?> GetDispatchDetailAsync(int id)
|
||||||
|
{
|
||||||
|
return await _dispatchDataService.GetDispatchDetailAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateDispatchAsync(UpdateDispatchInput input)
|
||||||
|
{
|
||||||
|
var dispatch = await _dispatchDataService.GetByIdAsync(input.Id);
|
||||||
|
if (dispatch == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (dispatch.Status != input.Status)
|
||||||
|
{
|
||||||
|
await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog
|
||||||
|
{
|
||||||
|
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
||||||
|
UserId = input.UserId,
|
||||||
|
FieldName = $"Dispatch {dispatch.DispatchNumber} Status",
|
||||||
|
OldValue = dispatch.Status ?? "",
|
||||||
|
NewValue = input.Status ?? "",
|
||||||
|
Action = "dispatch_status_change",
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch.Status = input.Status;
|
||||||
|
dispatch.NTEAmount = input.NTEAmount;
|
||||||
|
dispatch.ScheduledDate = input.ScheduledDate;
|
||||||
|
dispatch.CompletedDate = input.CompletedDate;
|
||||||
|
dispatch.Description = input.Description;
|
||||||
|
|
||||||
|
if (input.Status == "Completed" && dispatch.CompletedDate == null)
|
||||||
|
dispatch.CompletedDate = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _dispatchDataService.UpdateAsync(dispatch);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AddDispatchCommentResult?> AddDispatchCommentAsync(AddDispatchCommentInput input)
|
||||||
|
{
|
||||||
|
var dispatch = await _dispatchDataService.GetByIdWithVendorAndWorkOrderAsync(input.DispatchId);
|
||||||
|
if (dispatch == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var user = !string.IsNullOrEmpty(input.UserId) ? await _userDataService.GetByIdAsync(input.UserId) : null;
|
||||||
|
var senderName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "";
|
||||||
|
|
||||||
|
var comment = new Comments
|
||||||
|
{
|
||||||
|
CreatedDate = DateTime.UtcNow,
|
||||||
|
UserId = input.UserId,
|
||||||
|
WorkerOrderId = dispatch.WorkOrderId,
|
||||||
|
DispatchId = dispatch.Id,
|
||||||
|
Commenttext = input.Text,
|
||||||
|
CommentType = "dispatcher",
|
||||||
|
Commenter = !string.IsNullOrWhiteSpace(senderName) ? senderName : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await _commentDataService.AddAsync(comment);
|
||||||
|
|
||||||
|
if (input.SendEmail && dispatch.Vendor?.Email != null && dispatch.WorkOrder != null)
|
||||||
|
{
|
||||||
|
var subject = $"[WO-{dispatch.WorkOrder.InternalWONumber}-{dispatch.DispatchNumber}] {dispatch.WorkOrder.WorkerOrderTitle}";
|
||||||
|
var html = $"<p>{input.Text}</p><br/><p style='color:#666;font-size:13px;'>{senderName}<br/>Sea Haven Industries</p><hr/><p style='color:#999;font-size:12px;'>Reply to this email to respond.</p>";
|
||||||
|
await _emailPort.SendDispatchEmailAsync(dispatch.Vendor.Email, subject, html, dispatch.ReplyToAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AddDispatchCommentResult
|
||||||
|
{
|
||||||
|
Id = comment.Id,
|
||||||
|
CreatedDate = comment.CreatedDate,
|
||||||
|
Commenttext = comment.Commenttext,
|
||||||
|
CommentType = comment.CommentType,
|
||||||
|
UserName = user != null ? (user.FirstName + " " + user.LastName).Trim() : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<VerifyDispatchResult?> VerifyDispatchAsync(int dispatchId, string userId)
|
||||||
|
{
|
||||||
|
var dispatch = await _dispatchDataService.GetByIdAsync(dispatchId);
|
||||||
|
if (dispatch == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var checklistItems = await _dispatchDataService.GetChecklistItemsAsync(dispatchId);
|
||||||
|
var incompleteItems = checklistItems.Count(c => !c.IsCompleted);
|
||||||
|
|
||||||
|
var signoffs = await _dispatchDataService.GetSignoffsAsync(dispatchId);
|
||||||
|
var signoffList = signoffs.ToList();
|
||||||
|
var signoffCount = signoffList.Count;
|
||||||
|
var existingTypes = signoffList.Select(s => s.SignoffType).ToList();
|
||||||
|
|
||||||
|
var missing = new List<string>();
|
||||||
|
if (incompleteItems > 0)
|
||||||
|
missing.Add($"{incompleteItems} checklist item(s) incomplete");
|
||||||
|
if (signoffCount < 2)
|
||||||
|
{
|
||||||
|
if (!existingTypes.Contains("Customer")) missing.Add("Missing Customer sign-off");
|
||||||
|
if (!existingTypes.Contains("Vendor")) missing.Add("Missing Vendor sign-off");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.Count > 0)
|
||||||
|
throw new DispatchVerificationException(missing);
|
||||||
|
|
||||||
|
var user = userId != null ? await _userDataService.GetByIdAsync(userId) : null;
|
||||||
|
var userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "";
|
||||||
|
|
||||||
|
dispatch.Status = "Verified";
|
||||||
|
dispatch.VerifiedBy = userName;
|
||||||
|
dispatch.VerifiedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog
|
||||||
|
{
|
||||||
|
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
||||||
|
UserId = userId,
|
||||||
|
FieldName = $"Dispatch {dispatch.DispatchNumber}",
|
||||||
|
OldValue = dispatch.Status ?? "",
|
||||||
|
NewValue = "Verified",
|
||||||
|
Action = "dispatch_verified",
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
await _dispatchDataService.UpdateAsync(dispatch);
|
||||||
|
|
||||||
|
return new VerifyDispatchResult
|
||||||
|
{
|
||||||
|
Message = "Dispatch verified",
|
||||||
|
VerifiedBy = userName,
|
||||||
|
VerifiedAt = dispatch.VerifiedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AddDispatchSignoffResult?> AddDispatchSignoffAsync(AddDispatchSignoffInput input)
|
||||||
|
{
|
||||||
|
var dispatch = await _dispatchDataService.GetByIdAsync(input.DispatchId);
|
||||||
|
if (dispatch == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var existing = await _dispatchDataService.HasSignoffTypeAsync(input.DispatchId, input.SignoffType!);
|
||||||
|
if (existing)
|
||||||
|
throw new InvalidOperationException($"{input.SignoffType} sign-off already exists");
|
||||||
|
|
||||||
|
var signoff = new DispatchSignoff
|
||||||
|
{
|
||||||
|
DispatchId = input.DispatchId,
|
||||||
|
SignoffType = input.SignoffType,
|
||||||
|
Name = input.Name,
|
||||||
|
Signature = input.Signature,
|
||||||
|
SignatureMethod = input.SignatureMethod,
|
||||||
|
SignedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
await _dispatchDataService.AddSignoffAsync(signoff);
|
||||||
|
|
||||||
|
return new AddDispatchSignoffResult
|
||||||
|
{
|
||||||
|
Id = signoff.Id,
|
||||||
|
SignoffType = signoff.SignoffType,
|
||||||
|
Name = signoff.Name,
|
||||||
|
SignedAt = signoff.SignedAt,
|
||||||
|
SignatureMethod = signoff.SignatureMethod
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChecklistItemUpdateResult?> UpdateChecklistItemAsync(ChecklistItemUpdateInput input)
|
||||||
|
{
|
||||||
|
var item = await _dispatchDataService.GetChecklistItemByIdAsync(input.Id);
|
||||||
|
if (item == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
item.IsCompleted = input.IsCompleted;
|
||||||
|
item.CompletedBy = input.IsCompleted ? input.CompletedBy : null;
|
||||||
|
item.CompletedAt = input.IsCompleted ? DateTime.UtcNow : null;
|
||||||
|
await _dispatchDataService.UpdateChecklistItemAsync(item);
|
||||||
|
|
||||||
|
return new ChecklistItemUpdateResult
|
||||||
|
{
|
||||||
|
Message = "Updated",
|
||||||
|
Id = item.Id,
|
||||||
|
IsCompleted = item.IsCompleted,
|
||||||
|
CompletedBy = item.CompletedBy,
|
||||||
|
CompletedAt = item.CompletedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AddChecklistItemResult> AddChecklistItemAsync(AddChecklistItemInput input)
|
||||||
|
{
|
||||||
|
var checklistItems = await _dispatchDataService.GetChecklistItemsAsync(input.DispatchId);
|
||||||
|
var maxSort = checklistItems.Any() ? checklistItems.Max(c => c.SortOrder) : -1;
|
||||||
|
|
||||||
|
var item = new DispatchChecklistItem
|
||||||
|
{
|
||||||
|
DispatchId = input.DispatchId,
|
||||||
|
WorkOrderId = input.WorkOrderId,
|
||||||
|
ItemText = input.ItemText,
|
||||||
|
SortOrder = maxSort + 1,
|
||||||
|
IsCompleted = false
|
||||||
|
};
|
||||||
|
await _dispatchDataService.AddChecklistItemAsync(item);
|
||||||
|
|
||||||
|
return new AddChecklistItemResult
|
||||||
|
{
|
||||||
|
Id = item.Id,
|
||||||
|
ItemText = item.ItemText,
|
||||||
|
SortOrder = item.SortOrder,
|
||||||
|
IsCompleted = item.IsCompleted
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,47 +1,24 @@
|
||||||
using Data.SeaHavenIndustries;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using SeaHaven.Services.Interfaces;
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
namespace SeaHaven.Services.Implementation
|
namespace SeaHaven.Services.Implementation
|
||||||
{
|
{
|
||||||
public class WorkOrderFieldLockService : IWorkOrderFieldLockService
|
public class WorkOrderFieldLockService : IWorkOrderFieldLockService
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly IWorkOrderFieldLockDataService _dataService;
|
||||||
|
|
||||||
public WorkOrderFieldLockService(ApplicationDbContext context)
|
public WorkOrderFieldLockService(IWorkOrderFieldLockDataService dataService)
|
||||||
{
|
{
|
||||||
_context = context;
|
_dataService = dataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> IsLockedAsync(int workOrderId, string fieldName)
|
public Task<bool> IsLockedAsync(int workOrderId, string fieldName)
|
||||||
|
=> _dataService.IsLockedAsync(workOrderId, fieldName, CancellationToken.None);
|
||||||
|
|
||||||
|
public Task LockFieldAsync(int workOrderId, string fieldName, string? userId)
|
||||||
{
|
{
|
||||||
if (IsTracked(workOrderId, fieldName))
|
_dataService.EnqueueLock(workOrderId, fieldName, userId);
|
||||||
return true;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
return await _context.WorkOrderFieldLocks
|
|
||||||
.AnyAsync(l => l.WorkOrderId == workOrderId && l.FieldName == fieldName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LockFieldAsync(int workOrderId, string fieldName, string? userId)
|
|
||||||
{
|
|
||||||
if (IsTracked(workOrderId, fieldName))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (await IsLockedAsync(workOrderId, fieldName))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_context.WorkOrderFieldLocks.Add(new WorkOrderFieldLock
|
|
||||||
{
|
|
||||||
WorkOrderId = workOrderId,
|
|
||||||
FieldName = fieldName,
|
|
||||||
LockedAt = DateTime.UtcNow,
|
|
||||||
LockedByUserId = userId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsTracked(int workOrderId, string fieldName)
|
|
||||||
=> _context.WorkOrderFieldLocks.Local.Any(l =>
|
|
||||||
l.WorkOrderId == workOrderId &&
|
|
||||||
string.Equals(l.FieldName, fieldName, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using Data.SeaHavenIndustries;
|
||||||
using Data.SeaHavenIndustries.Enums;
|
using Data.SeaHavenIndustries.Enums;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using SeaHaven.DataServices.Models;
|
||||||
using SeaHaven.Services.DTOs;
|
using SeaHaven.Services.DTOs;
|
||||||
using SeaHaven.Services.Interfaces;
|
using SeaHaven.Services.Interfaces;
|
||||||
using SeaHaven.Services.Validation;
|
using SeaHaven.Services.Validation;
|
||||||
|
|
@ -11,15 +12,27 @@ namespace SeaHaven.Services.Implementation
|
||||||
public class WorkOrderService : IWorkOrderService
|
public class WorkOrderService : IWorkOrderService
|
||||||
{
|
{
|
||||||
private readonly IWorkOrderDataService _workOrderDataService;
|
private readonly IWorkOrderDataService _workOrderDataService;
|
||||||
|
private readonly ICommentDataService _commentDataService;
|
||||||
|
private readonly IUserDataService _userDataService;
|
||||||
|
private readonly IQuotesDataService _quotesDataService;
|
||||||
|
private readonly IFileStoragePort _fileStorage;
|
||||||
private readonly ICreateWorkOrderValidation _createValidator;
|
private readonly ICreateWorkOrderValidation _createValidator;
|
||||||
private readonly IUpdateWorkOrderValidation _updateValidator;
|
private readonly IUpdateWorkOrderValidation _updateValidator;
|
||||||
|
|
||||||
public WorkOrderService(
|
public WorkOrderService(
|
||||||
IWorkOrderDataService workOrderDataService,
|
IWorkOrderDataService workOrderDataService,
|
||||||
|
ICommentDataService commentDataService,
|
||||||
|
IUserDataService userDataService,
|
||||||
|
IQuotesDataService quotesDataService,
|
||||||
|
IFileStoragePort fileStorage,
|
||||||
ICreateWorkOrderValidation createValidator,
|
ICreateWorkOrderValidation createValidator,
|
||||||
IUpdateWorkOrderValidation updateValidator)
|
IUpdateWorkOrderValidation updateValidator)
|
||||||
{
|
{
|
||||||
_workOrderDataService = workOrderDataService;
|
_workOrderDataService = workOrderDataService;
|
||||||
|
_commentDataService = commentDataService;
|
||||||
|
_userDataService = userDataService;
|
||||||
|
_quotesDataService = quotesDataService;
|
||||||
|
_fileStorage = fileStorage;
|
||||||
_createValidator = createValidator;
|
_createValidator = createValidator;
|
||||||
_updateValidator = updateValidator;
|
_updateValidator = updateValidator;
|
||||||
}
|
}
|
||||||
|
|
@ -53,13 +66,12 @@ namespace SeaHaven.Services.Implementation
|
||||||
PageSize = pageSize
|
PageSize = pageSize
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId)
|
public async Task<int> CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId)
|
||||||
{
|
{
|
||||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||||
if (!validationResult.IsValid)
|
if (!validationResult.IsValid)
|
||||||
{
|
|
||||||
throw new ValidationException(validationResult.Errors);
|
throw new ValidationException(validationResult.Errors);
|
||||||
}
|
|
||||||
|
|
||||||
var workOrder = CreateWorkOrderDTO.MapToEntity(dto);
|
var workOrder = CreateWorkOrderDTO.MapToEntity(dto);
|
||||||
workOrder.createdby = userId;
|
workOrder.createdby = userId;
|
||||||
|
|
@ -67,9 +79,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
workOrder.InternalWONumber = await _workOrderDataService.GenerateNextInternalWONumberAsync();
|
workOrder.InternalWONumber = await _workOrderDataService.GenerateNextInternalWONumberAsync();
|
||||||
|
|
||||||
var saved = await _workOrderDataService.AddAsync(workOrder);
|
var saved = await _workOrderDataService.AddAsync(workOrder);
|
||||||
|
|
||||||
await LogAuditAsync(saved.Id, userId, AuditAction.Create, "Work order created");
|
await LogAuditAsync(saved.Id, userId, AuditAction.Create, "Work order created");
|
||||||
|
|
||||||
return saved.Id;
|
return saved.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,9 +87,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
{
|
{
|
||||||
var validationResult = await _updateValidator.ValidateAsync(dto);
|
var validationResult = await _updateValidator.ValidateAsync(dto);
|
||||||
if (!validationResult.IsValid)
|
if (!validationResult.IsValid)
|
||||||
{
|
|
||||||
throw new ValidationException(validationResult.Errors);
|
throw new ValidationException(validationResult.Errors);
|
||||||
}
|
|
||||||
|
|
||||||
var existingWorkOrder = await _workOrderDataService.GetByIdAsync(id);
|
var existingWorkOrder = await _workOrderDataService.GetByIdAsync(id);
|
||||||
if (existingWorkOrder == null)
|
if (existingWorkOrder == null)
|
||||||
|
|
@ -92,9 +100,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
await _workOrderDataService.UpdateAsync(existingWorkOrder);
|
await _workOrderDataService.UpdateAsync(existingWorkOrder);
|
||||||
|
|
||||||
if (changes.Any())
|
if (changes.Any())
|
||||||
{
|
|
||||||
await LogAuditAsync(id, userId, AuditAction.Edit, string.Join(", ", changes));
|
await LogAuditAsync(id, userId, AuditAction.Edit, string.Join(", ", changes));
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -108,61 +114,502 @@ namespace SeaHaven.Services.Implementation
|
||||||
await _workOrderDataService.DeleteWorkOrderContactsAsync(id);
|
await _workOrderDataService.DeleteWorkOrderContactsAsync(id);
|
||||||
await _workOrderDataService.DeleteWorkOrderCategoriesAsync(id);
|
await _workOrderDataService.DeleteWorkOrderCategoriesAsync(id);
|
||||||
await _workOrderDataService.DeleteAsync(id);
|
await _workOrderDataService.DeleteAsync(id);
|
||||||
|
|
||||||
await LogAuditAsync(id, userId, AuditAction.Delete, "Work order deleted");
|
await LogAuditAsync(id, userId, AuditAction.Delete, "Work order deleted");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> WorkOrderExistsAsync(int id) => await _workOrderDataService.ExistsAsync(id);
|
||||||
|
public async Task<string> GenerateNextInternalWONumberAsync() => await _workOrderDataService.GenerateNextInternalWONumberAsync();
|
||||||
|
public async Task<int> GetTotalWorkOrderCountAsync() => await _workOrderDataService.CountAsync();
|
||||||
|
|
||||||
|
public async Task<(IEnumerable<WorkOrderListItemReadModel> Items, int TotalCount)> GetWorkOrderListPagedAsync(
|
||||||
|
int page, int pageSize, string? search, string? status, string? assignee,
|
||||||
|
int? locationId, string? sortBy, string? sortDir)
|
||||||
|
{
|
||||||
|
return await _workOrderDataService.GetWorkOrderListPagedAsync(
|
||||||
|
page, pageSize, search, status, assignee, locationId, sortBy, sortDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id)
|
||||||
|
{
|
||||||
|
return await _workOrderDataService.GetWorkOrderDetailAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<FilteredWorkOrderPageDTO> GetFilteredWorkOrdersAsync(
|
||||||
|
string[] assignto, int[]? location, string[]? priority,
|
||||||
|
string[]? status, string[]? duedate, string search,
|
||||||
|
string sort, string sortby, int page, int pageSize)
|
||||||
|
{
|
||||||
|
var allWorkOrders = await _workOrderDataService.GetNonTemplateWorkOrdersWithLocationsAsync();
|
||||||
|
|
||||||
|
var data = allWorkOrders.AsEnumerable();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search))
|
||||||
|
{
|
||||||
|
var s = search.ToLower();
|
||||||
|
data = data.Where(w =>
|
||||||
|
(w.WorkerOrderTitle ?? "").ToLower().Contains(s) ||
|
||||||
|
(w.WorkerOrderNumber ?? "").ToLower().Contains(s) ||
|
||||||
|
(w.Locations != null && (w.Locations.Title ?? "").ToLower().Contains(s)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assignto != null && assignto.Length > 0 && assignto[0] != "" && assignto[0] != null)
|
||||||
|
data = data.Where(w => assignto.Contains(w.AssignTo));
|
||||||
|
|
||||||
|
if (location != null && location.Length > 0 && location[0] != 0)
|
||||||
|
data = data.Where(w => location.Contains(w.LocationId ?? 0));
|
||||||
|
|
||||||
|
if (priority != null && priority.Length > 0 && priority[0] != "" && priority[0] != null)
|
||||||
|
data = data.Where(w => priority.Contains(w.Priority));
|
||||||
|
|
||||||
|
if (status != null && status.Length > 0 && status[0] != "" && status[0] != null)
|
||||||
|
data = data.Where(w => status.Contains(w.Status));
|
||||||
|
|
||||||
|
var dataList = data.ToList();
|
||||||
|
|
||||||
|
dataList = dataList.Where(w =>
|
||||||
|
duedate == null || duedate.Length == 0 || duedate[0] == null ||
|
||||||
|
duedate.Any(dd => w.DueDate.HasValue && CheckDueDate(dd, w.DueDate.Value.Date, DateTime.UtcNow.Date))).ToList();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(sort) && !string.IsNullOrEmpty(sortby) && sortby != "")
|
||||||
|
{
|
||||||
|
switch (sortby)
|
||||||
|
{
|
||||||
|
case "Name":
|
||||||
|
dataList = sort.ToLower() == "ascending"
|
||||||
|
? dataList.OrderBy(w => w.WorkerOrderTitle).ToList()
|
||||||
|
: dataList.OrderByDescending(w => w.WorkerOrderTitle).ToList();
|
||||||
|
break;
|
||||||
|
case "Due Date":
|
||||||
|
dataList = sort.ToLower() == "ascending"
|
||||||
|
? dataList.OrderBy(w => w.DueDate).ToList()
|
||||||
|
: dataList.OrderByDescending(w => w.DueDate).ToList();
|
||||||
|
break;
|
||||||
|
case "Severity":
|
||||||
|
dataList = sort.ToLower() == "ascending"
|
||||||
|
? dataList.OrderBy(w => w.Priority).ToList()
|
||||||
|
: dataList.OrderByDescending(w => w.Priority).ToList();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = dataList.Count;
|
||||||
|
var pagedData = dataList
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(s => new FilteredWorkOrderReadModel
|
||||||
|
{
|
||||||
|
Id = s.Id,
|
||||||
|
WorkerOrderTitle = s.WorkerOrderTitle,
|
||||||
|
DueDate = s.DueDate,
|
||||||
|
Status = s.Status,
|
||||||
|
Priority = s.Priority,
|
||||||
|
Name = s.Locations != null ? s.Locations.Name : ""
|
||||||
|
})
|
||||||
|
.OrderByDescending(d => d.Name)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new FilteredWorkOrderPageDTO
|
||||||
|
{
|
||||||
|
Data = pagedData,
|
||||||
|
PageNumber = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<FilteredWorkOrderPageDTO> GetFilteredWorkOrders2Async(
|
||||||
|
string[] assignto, int[]? location, string[]? priority,
|
||||||
|
string[]? status, string[]? duedate, string search,
|
||||||
|
string sort, string sortby, int page, int pageSize)
|
||||||
|
{
|
||||||
|
location ??= Array.Empty<int>();
|
||||||
|
priority ??= Array.Empty<string>();
|
||||||
|
status ??= Array.Empty<string>();
|
||||||
|
duedate ??= Array.Empty<string>();
|
||||||
|
search ??= string.Empty;
|
||||||
|
|
||||||
|
var allWorkOrders = await _workOrderDataService.GetNonTemplateWorkOrdersWithLocationsAsync();
|
||||||
|
var totalCount = allWorkOrders.Count;
|
||||||
|
|
||||||
|
var currentDate = DateTime.UtcNow.Date;
|
||||||
|
var data = allWorkOrders.Where(w =>
|
||||||
|
(((w.WorkerOrderTitle ?? "").Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(w.WorkerOrderNumber ?? "").Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(w.Locations != null &&
|
||||||
|
(w.Locations.Title ?? "").Contains(search, StringComparison.OrdinalIgnoreCase))) &&
|
||||||
|
(assignto.Length == 0 || assignto.Contains(w.AssignTo)) &&
|
||||||
|
(location.Length == 0 || (w.LocationId.HasValue && location.Contains(w.LocationId.Value))) &&
|
||||||
|
(priority.Length == 0 || priority.Contains(w.Priority)) &&
|
||||||
|
(status.Length == 0 || status.Contains(w.Status)))).ToList();
|
||||||
|
|
||||||
|
data = data.Where(w =>
|
||||||
|
duedate.Length == 0 ||
|
||||||
|
duedate.Any(dd => w.DueDate.HasValue && CheckDueDate(dd, w.DueDate.Value.Date, currentDate))).ToList();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(sort) && !string.IsNullOrEmpty(sortby) && sortby != "")
|
||||||
|
{
|
||||||
|
switch (sortby)
|
||||||
|
{
|
||||||
|
case "Name":
|
||||||
|
data = sort.ToLower() == "ascending"
|
||||||
|
? data.OrderBy(w => w.WorkerOrderTitle).ToList()
|
||||||
|
: data.OrderByDescending(w => w.WorkerOrderTitle).ToList();
|
||||||
|
break;
|
||||||
|
case "Due Date":
|
||||||
|
data = sort.ToLower() == "ascending"
|
||||||
|
? data.OrderBy(w => w.DueDate).ToList()
|
||||||
|
: data.OrderByDescending(w => w.DueDate).ToList();
|
||||||
|
break;
|
||||||
|
case "Severity":
|
||||||
|
data = sort.ToLower() == "ascending"
|
||||||
|
? data.OrderBy(w => w.Priority).ToList()
|
||||||
|
: data.OrderByDescending(w => w.Priority).ToList();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newdata = data
|
||||||
|
.Select(s => new FilteredWorkOrderReadModel
|
||||||
|
{
|
||||||
|
Id = s.Id,
|
||||||
|
WorkerOrderTitle = s.WorkerOrderTitle,
|
||||||
|
DueDate = s.DueDate,
|
||||||
|
Status = s.Status,
|
||||||
|
Priority = s.Priority,
|
||||||
|
Name = s.Locations != null ? s.Locations.Name : ""
|
||||||
|
})
|
||||||
|
.OrderByDescending(d => d.Name)
|
||||||
|
.ToList()
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new FilteredWorkOrderPageDTO
|
||||||
|
{
|
||||||
|
Data = newdata,
|
||||||
|
PageNumber = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WorkOrderDTO?> ChangeStatusAsync(int id, string status, string userId)
|
||||||
|
{
|
||||||
|
var workOrder = await _workOrderDataService.GetByIdAsync(id);
|
||||||
|
if (workOrder == null) return null;
|
||||||
|
|
||||||
|
if (workOrder.Status != status)
|
||||||
|
{
|
||||||
|
await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog
|
||||||
|
{
|
||||||
|
WorkOrderId = id,
|
||||||
|
UserId = userId,
|
||||||
|
FieldName = "Status",
|
||||||
|
OldValue = workOrder.Status ?? "",
|
||||||
|
NewValue = status,
|
||||||
|
Action = AuditAction.StatusChange.ToString(),
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
workOrder.Status = status;
|
||||||
|
await _workOrderDataService.UpdateAsync(workOrder);
|
||||||
|
|
||||||
|
return WorkOrderDTO.MapToModel(workOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChangeAssignmentResult?> ChangeAssignmentAsync(int id, string? assignTo, string userId)
|
||||||
|
{
|
||||||
|
var workOrder = await _workOrderDataService.GetByIdAsync(id);
|
||||||
|
if (workOrder == null) return null;
|
||||||
|
|
||||||
|
if (workOrder.AssignTo != assignTo)
|
||||||
|
{
|
||||||
|
var oldUser = workOrder.AssignTo != null ? await _userDataService.GetByIdAsync(workOrder.AssignTo) : null;
|
||||||
|
var newUser = assignTo != null ? await _userDataService.GetByIdAsync(assignTo) : null;
|
||||||
|
|
||||||
|
await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog
|
||||||
|
{
|
||||||
|
WorkOrderId = id,
|
||||||
|
UserId = userId,
|
||||||
|
FieldName = "AssignedTo",
|
||||||
|
OldValue = oldUser != null ? (oldUser.FirstName + " " + oldUser.LastName).Trim() : "",
|
||||||
|
NewValue = newUser != null ? (newUser.FirstName + " " + newUser.LastName).Trim() : "Unassigned",
|
||||||
|
Action = AuditAction.AssignmentChange.ToString(),
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
workOrder.AssignTo = assignTo;
|
||||||
|
await _workOrderDataService.UpdateAsync(workOrder);
|
||||||
|
|
||||||
|
var assignedUser = assignTo != null ? await _userDataService.GetByIdAsync(assignTo) : null;
|
||||||
|
return new ChangeAssignmentResult
|
||||||
|
{
|
||||||
|
Message = "Updated Successfully",
|
||||||
|
AssignedTo = assignedUser != null ? (assignedUser.FirstName + " " + assignedUser.LastName).Trim() : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> CreateWorkOrderWithDetailsAsync(CreateWorkOrderWithDetailsInput input, string userId)
|
||||||
|
{
|
||||||
|
var createDto = new CreateWorkOrderDTO
|
||||||
|
{
|
||||||
|
WorkerOrderTitle = input.WorkerOrderTitle,
|
||||||
|
Description = input.Description,
|
||||||
|
Status = WorkOrderStatus.Open,
|
||||||
|
Priority = Enum.TryParse<WorkOrderPriority>(input.Priority, out var p) ? p : WorkOrderPriority.Medium,
|
||||||
|
LocationId = input.LocationId,
|
||||||
|
AssignTo = input.AssignTo,
|
||||||
|
DueDate = input.DueDate
|
||||||
|
};
|
||||||
|
|
||||||
|
var workOrderId = await CreateWorkOrderAsync(createDto, userId);
|
||||||
|
|
||||||
|
if (input.ContactIds != null && input.ContactIds.Any())
|
||||||
|
await _workOrderDataService.AddWorkOrderContactsAsync(workOrderId, input.ContactIds);
|
||||||
|
|
||||||
|
if (input.CategoryIds != null && input.CategoryIds.Any())
|
||||||
|
await _workOrderDataService.AddWorkOrderCategoriesAsync(workOrderId, input.CategoryIds);
|
||||||
|
|
||||||
|
await SaveAttachmentsAndPhotos(workOrderId, input.Attachments, input.BeforPhotoAttachment,
|
||||||
|
input.AfterPhotoAttachment, input.SignOffAttachment);
|
||||||
|
|
||||||
|
return workOrderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateWorkOrderWithDetailsAsync(UpdateWorkOrderWithDetailsInput input, string userId)
|
||||||
|
{
|
||||||
|
var updateDto = new UpdateWorkOrderDTO
|
||||||
|
{
|
||||||
|
WorkerOrderTitle = input.WorkerOrderTitle,
|
||||||
|
Description = input.Description,
|
||||||
|
Status = Enum.TryParse<WorkOrderStatus>(input.Status, out var st) ? st : null,
|
||||||
|
Priority = Enum.TryParse<WorkOrderPriority>(input.Priority, out var pr) ? pr : null,
|
||||||
|
LocationId = input.LocationId,
|
||||||
|
AssignTo = input.AssignTo,
|
||||||
|
DueDate = input.DueDate,
|
||||||
|
CompletedDate = input.CompletedDate
|
||||||
|
};
|
||||||
|
|
||||||
|
var success = await UpdateWorkOrderAsync(input.Id, updateDto, userId);
|
||||||
|
if (!success) return false;
|
||||||
|
|
||||||
|
await _workOrderDataService.DeleteWorkOrderContactsAsync(input.Id);
|
||||||
|
await _workOrderDataService.DeleteWorkOrderCategoriesAsync(input.Id);
|
||||||
|
|
||||||
|
if (input.ContactIds != null && input.ContactIds.Any())
|
||||||
|
await _workOrderDataService.AddWorkOrderContactsAsync(input.Id, input.ContactIds);
|
||||||
|
|
||||||
|
if (input.CategoryIds != null && input.CategoryIds.Any())
|
||||||
|
await _workOrderDataService.AddWorkOrderCategoriesAsync(input.Id, input.CategoryIds);
|
||||||
|
|
||||||
|
await SaveAttachmentsAndPhotos(input.Id, input.Attachments, input.BeforPhotoAttachment,
|
||||||
|
input.AfterPhotoAttachment, input.SignOffAttachment);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> WorkOrderExistsAsync(int id)
|
public async Task<bool> DeleteWorkOrderCascadeAsync(int id, string userId)
|
||||||
{
|
{
|
||||||
return await _workOrderDataService.ExistsAsync(id);
|
var success = await DeleteWorkOrderAsync(id, userId);
|
||||||
|
if (!success) return false;
|
||||||
|
|
||||||
|
await _quotesDataService.UnlinkFromWorkOrderAsync(id);
|
||||||
|
|
||||||
|
var comments = await _commentDataService.GetByWorkOrderIdAsync(id);
|
||||||
|
foreach (var comment in comments)
|
||||||
|
await _commentDataService.DeleteAsync(comment.Id);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GenerateNextInternalWONumberAsync()
|
public async Task<Comments> AddCommentAsync(AddCommentInput input, string userId)
|
||||||
{
|
{
|
||||||
return await _workOrderDataService.GenerateNextInternalWONumberAsync();
|
var comment = new Comments
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
WorkerOrderId = input.WorkorderId,
|
||||||
|
Commenttext = input.Text,
|
||||||
|
CommentType = input.CommentType ?? "vendor"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (input.Document != null)
|
||||||
|
{
|
||||||
|
var fileUrl = await _fileStorage.SaveFileAsync(input.Document);
|
||||||
|
comment.Documents = fileUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _commentDataService.AddAsync(comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> GetTotalWorkOrderCountAsync()
|
public async Task<CommentResult> AddCommentJsonAsync(AddCommentInput input, string userId)
|
||||||
{
|
{
|
||||||
return await _workOrderDataService.CountAsync();
|
var comment = new Comments
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
WorkerOrderId = input.WorkorderId,
|
||||||
|
Commenttext = input.Text,
|
||||||
|
CommentType = input.CommentType ?? "internal"
|
||||||
|
};
|
||||||
|
|
||||||
|
var saved = await _commentDataService.AddAsync(comment);
|
||||||
|
var user = await _userDataService.GetByIdAsync(userId);
|
||||||
|
|
||||||
|
return new CommentResult
|
||||||
|
{
|
||||||
|
Id = saved.Id,
|
||||||
|
CreatedDate = saved.CreatedDate,
|
||||||
|
Commenttext = saved.Commenttext,
|
||||||
|
CommentType = saved.CommentType,
|
||||||
|
UserName = user != null ? (user.FirstName + " " + user.LastName).Trim() : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CommentListItemReadModel>> GetCommentsAsync()
|
||||||
|
{
|
||||||
|
var comments = await _commentDataService.GetAllAsync();
|
||||||
|
return comments.Select(s => new CommentListItemReadModel
|
||||||
|
{
|
||||||
|
Id = s.Id,
|
||||||
|
Commenttext = s.Commenttext,
|
||||||
|
FirstName = s.ApplicationUser != null ? s.ApplicationUser.FirstName : "",
|
||||||
|
Documents = s.Documents
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CommentListItemReadModel>> GetCommentsByWorkorderIdAsync(int woid)
|
||||||
|
{
|
||||||
|
var comments = await _commentDataService.GetByWorkOrderIdAsync(woid);
|
||||||
|
return comments.Select(s => new CommentListItemReadModel
|
||||||
|
{
|
||||||
|
Id = s.Id,
|
||||||
|
Commenttext = s.Commenttext,
|
||||||
|
FirstName = s.ApplicationUser != null ? s.ApplicationUser.FirstName : "",
|
||||||
|
Documents = s.Documents
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<WorkOrder>> GetWorkordersDDAsync()
|
||||||
|
{
|
||||||
|
var data = await _workOrderDataService.GetAllAsync();
|
||||||
|
return data.Where(w => w.istemplate != true).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<WorkorderFilterVM>> GetWorkordersAsync()
|
||||||
|
{
|
||||||
|
var workOrders = await _workOrderDataService.GetAllWithDetailsAsync();
|
||||||
|
return workOrders.Select(s => new WorkorderFilterVM
|
||||||
|
{
|
||||||
|
Id = s.Id,
|
||||||
|
Priority = s.Priority,
|
||||||
|
Status = s.Status,
|
||||||
|
WorkerOrderNumber = s.WorkerOrderNumber,
|
||||||
|
WorkerOrderTitle = s.WorkerOrderTitle,
|
||||||
|
DueDate = s.DueDate,
|
||||||
|
AssignToUserName = s.AssignToUser != null ? s.AssignToUser.FirstName : "",
|
||||||
|
LocationName = s.Locations != null ? s.Locations.Name : "",
|
||||||
|
LocationId = s.LocationId,
|
||||||
|
PO = s.PO,
|
||||||
|
TT = s.TT
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveAttachmentsAndPhotos(int workOrderId,
|
||||||
|
List<Microsoft.AspNetCore.Http.IFormFile>? attachments,
|
||||||
|
Microsoft.AspNetCore.Http.IFormFile? beforPhoto,
|
||||||
|
Microsoft.AspNetCore.Http.IFormFile? afterPhoto,
|
||||||
|
Microsoft.AspNetCore.Http.IFormFile? signOffPhoto)
|
||||||
|
{
|
||||||
|
if (attachments != null)
|
||||||
|
{
|
||||||
|
foreach (var attachment in attachments)
|
||||||
|
{
|
||||||
|
var fileUrl = await _fileStorage.SaveFileAsync(attachment);
|
||||||
|
await _workOrderDataService.AddWorkOrderAttachmentAsync(new WorkOrderAttachments
|
||||||
|
{
|
||||||
|
WorkorderId = workOrderId,
|
||||||
|
Attachments = fileUrl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beforPhoto != null)
|
||||||
|
{
|
||||||
|
var fileUrl = await _fileStorage.SaveFileAsync(beforPhoto);
|
||||||
|
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
|
||||||
|
if (workOrder != null)
|
||||||
|
{
|
||||||
|
workOrder.BeforPhotoAttachment = fileUrl;
|
||||||
|
await _workOrderDataService.UpdateAsync(workOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterPhoto != null)
|
||||||
|
{
|
||||||
|
var fileUrl = await _fileStorage.SaveFileAsync(afterPhoto);
|
||||||
|
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
|
||||||
|
if (workOrder != null)
|
||||||
|
{
|
||||||
|
workOrder.AfterPhotoAttachment = fileUrl;
|
||||||
|
await _workOrderDataService.UpdateAsync(workOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signOffPhoto != null)
|
||||||
|
{
|
||||||
|
var fileUrl = await _fileStorage.SaveFileAsync(signOffPhoto);
|
||||||
|
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
|
||||||
|
if (workOrder != null)
|
||||||
|
{
|
||||||
|
workOrder.SignOffAttachment = fileUrl;
|
||||||
|
await _workOrderDataService.UpdateAsync(workOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CheckDueDate(string dd, DateTime dueDate, DateTime currentDate)
|
||||||
|
{
|
||||||
|
return dd switch
|
||||||
|
{
|
||||||
|
"Today" => dueDate == currentDate,
|
||||||
|
"Tomorrow" => dueDate == currentDate.AddDays(1),
|
||||||
|
"Next Week" => dueDate >= currentDate.AddDays(1) && dueDate <= currentDate.AddDays(7),
|
||||||
|
"Next Month" => dueDate >= currentDate.AddDays(1) && dueDate <= currentDate.AddMonths(1),
|
||||||
|
"Over Due" => dueDate < currentDate,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<string> TrackChanges(WorkOrder existing, UpdateWorkOrderDTO dto)
|
private List<string> TrackChanges(WorkOrder existing, UpdateWorkOrderDTO dto)
|
||||||
{
|
{
|
||||||
var changes = new List<string>();
|
var changes = new List<string>();
|
||||||
|
|
||||||
if (dto.WorkerOrderTitle != null && dto.WorkerOrderTitle != existing.WorkerOrderTitle)
|
if (dto.WorkerOrderTitle != null && dto.WorkerOrderTitle != existing.WorkerOrderTitle)
|
||||||
changes.Add($"Title: {existing.WorkerOrderTitle} → {dto.WorkerOrderTitle}");
|
changes.Add($"Title: {existing.WorkerOrderTitle} → {dto.WorkerOrderTitle}");
|
||||||
|
|
||||||
if (dto.Status.HasValue && dto.Status.ToString() != existing.Status)
|
if (dto.Status.HasValue && dto.Status.ToString() != existing.Status)
|
||||||
changes.Add($"Status: {existing.Status} → {dto.Status}");
|
changes.Add($"Status: {existing.Status} → {dto.Status}");
|
||||||
|
|
||||||
if (dto.Priority.HasValue && dto.Priority.ToString() != existing.Priority)
|
if (dto.Priority.HasValue && dto.Priority.ToString() != existing.Priority)
|
||||||
changes.Add($"Priority: {existing.Priority} → {dto.Priority}");
|
changes.Add($"Priority: {existing.Priority} → {dto.Priority}");
|
||||||
|
|
||||||
if (dto.AssignTo != null && dto.AssignTo != existing.AssignTo)
|
if (dto.AssignTo != null && dto.AssignTo != existing.AssignTo)
|
||||||
changes.Add($"Assigned To: {existing.AssignTo} → {dto.AssignTo}");
|
changes.Add($"Assigned To: {existing.AssignTo} → {dto.AssignTo}");
|
||||||
|
|
||||||
if (dto.DueDate.HasValue && dto.DueDate != existing.DueDate)
|
if (dto.DueDate.HasValue && dto.DueDate != existing.DueDate)
|
||||||
changes.Add($"Due Date: {existing.DueDate:yyyy-MM-dd} → {dto.DueDate:yyyy-MM-dd}");
|
changes.Add($"Due Date: {existing.DueDate:yyyy-MM-dd} → {dto.DueDate:yyyy-MM-dd}");
|
||||||
|
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LogAuditAsync(int workOrderId, string userId, AuditAction action, string description)
|
private async Task LogAuditAsync(int workOrderId, string userId, AuditAction action, string description)
|
||||||
{
|
{
|
||||||
var auditLog = new WorkOrderAuditLog
|
await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog
|
||||||
{
|
{
|
||||||
WorkOrderId = workOrderId,
|
WorkOrderId = workOrderId,
|
||||||
Action = action.ToString(),
|
Action = action.ToString(),
|
||||||
FieldName = description,
|
FieldName = description,
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
});
|
||||||
|
|
||||||
await _workOrderDataService.AddAuditLogAsync(auditLog);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
16
SeaHaven.Services/Interfaces/IQuotesService.cs
Normal file
16
SeaHaven.Services/Interfaces/IQuotesService.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Interfaces
|
||||||
|
{
|
||||||
|
public interface IQuotesService
|
||||||
|
{
|
||||||
|
Task CreateQuoteAsync(CreateQuoteInput input, CancellationToken cancellationToken);
|
||||||
|
Task<bool> UpdateQuoteAsync(UpdateQuoteInput input, CancellationToken cancellationToken);
|
||||||
|
Task<bool> DeleteQuoteAsync(int id, CancellationToken cancellationToken);
|
||||||
|
Task<PagedQuotesResult> GetQuotesPagedAsync(int page, int pageSize, string search, CancellationToken cancellationToken);
|
||||||
|
Task<IReadOnlyList<WorkOrderQuoteDetailDTO>> GetWorkOrderQuoteDetailsAsync(
|
||||||
|
int woId, CancellationToken cancellationToken);
|
||||||
|
Task<QuoteDetailDTO?> GetQuoteDetailsByIdAsync(int id, CancellationToken cancellationToken);
|
||||||
|
Task<QuoteReportingData> GetReportingAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
SeaHaven.Services/Interfaces/IServicePorts.cs
Normal file
19
SeaHaven.Services/Interfaces/IServicePorts.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Interfaces
|
||||||
|
{
|
||||||
|
public interface IFileStoragePort
|
||||||
|
{
|
||||||
|
Task<string> SaveFileAsync(IFormFile file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IDispatchEmailPort
|
||||||
|
{
|
||||||
|
Task<bool> SendDispatchEmailAsync(string emailTo, string subject, string htmlBody, string? replyTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IVendorTokenPort
|
||||||
|
{
|
||||||
|
Task<string> GetOrCreateActiveTokenAsync(int vendorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
SeaHaven.Services/Interfaces/ISyncExternalSource.cs
Normal file
9
SeaHaven.Services/Interfaces/ISyncExternalSource.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace SeaHaven.Services.Interfaces
|
||||||
|
{
|
||||||
|
public interface ISyncExternalSource
|
||||||
|
{
|
||||||
|
IAsyncEnumerable<Dictionary<string, string?>> ScanWorkOrdersAsync(CancellationToken cancellationToken);
|
||||||
|
IAsyncEnumerable<Dictionary<string, string?>> ScanCommentsAsync(CancellationToken cancellationToken);
|
||||||
|
IAsyncEnumerable<Dictionary<string, string?>> ScanVendorRepliesAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
SeaHaven.Services/Interfaces/ISyncService.cs
Normal file
15
SeaHaven.Services/Interfaces/ISyncService.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Interfaces
|
||||||
|
{
|
||||||
|
public interface ISyncService
|
||||||
|
{
|
||||||
|
Task<SyncWorkOrdersResult> SyncWorkOrdersAsync(CancellationToken cancellationToken);
|
||||||
|
Task<SyncCommentsResult> SyncCommentsAsync(CancellationToken cancellationToken);
|
||||||
|
Task<BackfillResult> BackfillInternalWONumbersAsync(CancellationToken cancellationToken);
|
||||||
|
Task<BackfillResult> BackfillDispatchNumbersAsync(CancellationToken cancellationToken);
|
||||||
|
Task<SyncCommentsResult> SyncVendorRepliesAsync(CancellationToken cancellationToken);
|
||||||
|
Task<BackfillResult> BackfillCommentTypesAsync(CancellationToken cancellationToken);
|
||||||
|
Task<SyncAllResult> SyncAllAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
SeaHaven.Services/Interfaces/IWorkOrderDispatchService.cs
Normal file
18
SeaHaven.Services/Interfaces/IWorkOrderDispatchService.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
using SeaHaven.DataServices.Models;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Interfaces
|
||||||
|
{
|
||||||
|
public interface IWorkOrderDispatchService
|
||||||
|
{
|
||||||
|
Task<DispatchToVendorResult> DispatchToVendorAsync(DispatchToVendorInput input);
|
||||||
|
Task<IEnumerable<DispatchReadModel>> GetDispatchesAsync(int workOrderId);
|
||||||
|
Task<DispatchDetailReadModel?> GetDispatchDetailAsync(int id);
|
||||||
|
Task<bool> UpdateDispatchAsync(UpdateDispatchInput input);
|
||||||
|
Task<AddDispatchCommentResult?> AddDispatchCommentAsync(AddDispatchCommentInput input);
|
||||||
|
Task<VerifyDispatchResult?> VerifyDispatchAsync(int dispatchId, string userId);
|
||||||
|
Task<AddDispatchSignoffResult?> AddDispatchSignoffAsync(AddDispatchSignoffInput input);
|
||||||
|
Task<ChecklistItemUpdateResult?> UpdateChecklistItemAsync(ChecklistItemUpdateInput input);
|
||||||
|
Task<AddChecklistItemResult> AddChecklistItemAsync(AddChecklistItemInput input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using SeaHaven.DataServices.Models;
|
||||||
using SeaHaven.Services.DTOs;
|
using SeaHaven.Services.DTOs;
|
||||||
|
|
||||||
namespace SeaHaven.Services.Interfaces
|
namespace SeaHaven.Services.Interfaces
|
||||||
|
|
@ -14,5 +18,27 @@ namespace SeaHaven.Services.Interfaces
|
||||||
Task<bool> WorkOrderExistsAsync(int id);
|
Task<bool> WorkOrderExistsAsync(int id);
|
||||||
Task<string> GenerateNextInternalWONumberAsync();
|
Task<string> GenerateNextInternalWONumberAsync();
|
||||||
Task<int> GetTotalWorkOrderCountAsync();
|
Task<int> GetTotalWorkOrderCountAsync();
|
||||||
|
|
||||||
|
Task<(IEnumerable<WorkOrderListItemReadModel> Items, int TotalCount)> GetWorkOrderListPagedAsync(
|
||||||
|
int page, int pageSize, string? search, string? status, string? assignee,
|
||||||
|
int? locationId, string? sortBy, string? sortDir);
|
||||||
|
Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id);
|
||||||
|
Task<FilteredWorkOrderPageDTO> GetFilteredWorkOrdersAsync(
|
||||||
|
string[] assignto, int[]? location, string[]? priority,
|
||||||
|
string[]? status, string[]? duedate, string search, string sort, string sortby, int page, int pageSize);
|
||||||
|
Task<FilteredWorkOrderPageDTO> GetFilteredWorkOrders2Async(
|
||||||
|
string[] assignto, int[]? location, string[]? priority,
|
||||||
|
string[]? status, string[]? duedate, string search, string sort, string sortby, int page, int pageSize);
|
||||||
|
Task<WorkOrderDTO?> ChangeStatusAsync(int id, string status, string userId);
|
||||||
|
Task<ChangeAssignmentResult?> ChangeAssignmentAsync(int id, string? assignTo, string userId);
|
||||||
|
Task<int> CreateWorkOrderWithDetailsAsync(CreateWorkOrderWithDetailsInput input, string userId);
|
||||||
|
Task<bool> UpdateWorkOrderWithDetailsAsync(UpdateWorkOrderWithDetailsInput input, string userId);
|
||||||
|
Task<bool> DeleteWorkOrderCascadeAsync(int id, string userId);
|
||||||
|
Task<Comments> AddCommentAsync(AddCommentInput input, string userId);
|
||||||
|
Task<CommentResult> AddCommentJsonAsync(AddCommentInput input, string userId);
|
||||||
|
Task<IEnumerable<CommentListItemReadModel>> GetCommentsAsync();
|
||||||
|
Task<IEnumerable<CommentListItemReadModel>> GetCommentsByWorkorderIdAsync(int woid);
|
||||||
|
Task<IEnumerable<WorkOrder>> GetWorkordersDDAsync();
|
||||||
|
Task<IEnumerable<WorkorderFilterVM>> GetWorkordersAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Data.SeaHavenIndustries.Enums;
|
using Data.SeaHavenIndustries.Enums;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SeaHaven.DataServices.Implementation;
|
||||||
using SeaHaven.Services.Constants;
|
using SeaHaven.Services.Constants;
|
||||||
using SeaHaven.Services.Implementation;
|
using SeaHaven.Services.Implementation;
|
||||||
using SeaHaven.Services.Interfaces;
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
@ -17,6 +18,12 @@ public class SyncFieldMergePolicyTests
|
||||||
return new ApplicationDbContext(options);
|
return new ApplicationDbContext(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static WorkOrderFieldLockService CreateLocks(ApplicationDbContext context)
|
||||||
|
=> new(new WorkOrderFieldLockDataService(context));
|
||||||
|
|
||||||
|
private static WorkOrderAuditService CreateAudit(ApplicationDbContext context, WorkOrderFieldLockService locks)
|
||||||
|
=> new(new WorkOrderAuditDataService(context), locks);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TryApply_WhenFieldLocked_RejectsAndAudits()
|
public async Task TryApply_WhenFieldLocked_RejectsAndAudits()
|
||||||
{
|
{
|
||||||
|
|
@ -31,8 +38,8 @@ public class SyncFieldMergePolicyTests
|
||||||
});
|
});
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
var locks = new WorkOrderFieldLockService(context);
|
var locks = CreateLocks(context);
|
||||||
var audit = new WorkOrderAuditService(context, locks);
|
var audit = CreateAudit(context, locks);
|
||||||
var policy = new SyncFieldMergePolicy(locks);
|
var policy = new SyncFieldMergePolicy(locks);
|
||||||
|
|
||||||
var syncContext = new WorkOrderSyncContext
|
var syncContext = new WorkOrderSyncContext
|
||||||
|
|
@ -61,8 +68,8 @@ public class SyncFieldMergePolicyTests
|
||||||
context.workOrders.Add(wo);
|
context.workOrders.Add(wo);
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
var locks = new WorkOrderFieldLockService(context);
|
var locks = CreateLocks(context);
|
||||||
var audit = new WorkOrderAuditService(context, locks);
|
var audit = CreateAudit(context, locks);
|
||||||
var policy = new SyncFieldMergePolicy(locks);
|
var policy = new SyncFieldMergePolicy(locks);
|
||||||
|
|
||||||
var syncContext = new WorkOrderSyncContext
|
var syncContext = new WorkOrderSyncContext
|
||||||
|
|
@ -86,8 +93,8 @@ public class SyncFieldMergePolicyTests
|
||||||
context.workOrders.Add(wo);
|
context.workOrders.Add(wo);
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
var locks = new WorkOrderFieldLockService(context);
|
var locks = CreateLocks(context);
|
||||||
var audit = new WorkOrderAuditService(context, locks);
|
var audit = CreateAudit(context, locks);
|
||||||
var policy = new SyncFieldMergePolicy(locks);
|
var policy = new SyncFieldMergePolicy(locks);
|
||||||
|
|
||||||
var syncContext = new WorkOrderSyncContext
|
var syncContext = new WorkOrderSyncContext
|
||||||
|
|
@ -113,8 +120,8 @@ public class SyncFieldMergePolicyTests
|
||||||
context.workOrders.Add(wo);
|
context.workOrders.Add(wo);
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
var locks = new WorkOrderFieldLockService(context);
|
var locks = CreateLocks(context);
|
||||||
var audit = new WorkOrderAuditService(context, locks);
|
var audit = CreateAudit(context, locks);
|
||||||
var policy = new SyncFieldMergePolicy(locks);
|
var policy = new SyncFieldMergePolicy(locks);
|
||||||
|
|
||||||
var syncContext = new WorkOrderSyncContext
|
var syncContext = new WorkOrderSyncContext
|
||||||
|
|
@ -141,7 +148,7 @@ public class SyncFieldMergePolicyTests
|
||||||
});
|
});
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
var locks = new WorkOrderFieldLockService(context);
|
var locks = CreateLocks(context);
|
||||||
var policy = new SyncFieldMergePolicy(locks);
|
var policy = new SyncFieldMergePolicy(locks);
|
||||||
|
|
||||||
var canOverwrite = await policy.CanSyncOverwriteAsync(20, WorkOrderFieldNames.AssignTo);
|
var canOverwrite = await policy.CanSyncOverwriteAsync(20, WorkOrderFieldNames.AssignTo);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Data.SeaHavenIndustries.Enums;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SeaHaven.DataServices.Implementation;
|
||||||
using SeaHaven.Services.Constants;
|
using SeaHaven.Services.Constants;
|
||||||
using SeaHaven.Services.Implementation;
|
using SeaHaven.Services.Implementation;
|
||||||
using SeaHaven.Services.Interfaces;
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
@ -17,6 +17,15 @@ public class WorkOrderAuditServiceTests
|
||||||
return new ApplicationDbContext(options);
|
return new ApplicationDbContext(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static (WorkOrderAuditService audit, ApplicationDbContext context) CreateAudit(ApplicationDbContext context)
|
||||||
|
{
|
||||||
|
var lockData = new WorkOrderFieldLockDataService(context);
|
||||||
|
var locks = new WorkOrderFieldLockService(lockData);
|
||||||
|
var auditData = new WorkOrderAuditDataService(context);
|
||||||
|
var audit = new WorkOrderAuditService(auditData, locks);
|
||||||
|
return (audit, context);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task LogStatusChanged_CreatesSingleAuditWithManualEventType()
|
public async Task LogStatusChanged_CreatesSingleAuditWithManualEventType()
|
||||||
{
|
{
|
||||||
|
|
@ -24,8 +33,7 @@ public class WorkOrderAuditServiceTests
|
||||||
context.workOrders.Add(new WorkOrder { Id = 1, Status = "Open" });
|
context.workOrders.Add(new WorkOrder { Id = 1, Status = "Open" });
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
var locks = new WorkOrderFieldLockService(context);
|
var (audit, _) = CreateAudit(context);
|
||||||
var audit = new WorkOrderAuditService(context, locks);
|
|
||||||
|
|
||||||
await audit.LogStatusChangedAsync(1, "Open", "In Progress", "user-1");
|
await audit.LogStatusChangedAsync(1, "Open", "In Progress", "user-1");
|
||||||
|
|
||||||
|
|
@ -44,8 +52,7 @@ public class WorkOrderAuditServiceTests
|
||||||
context.workOrders.Add(new WorkOrder { Id = 2, Status = "Open" });
|
context.workOrders.Add(new WorkOrder { Id = 2, Status = "Open" });
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
var locks = new WorkOrderFieldLockService(context);
|
var (audit, _) = CreateAudit(context);
|
||||||
var audit = new WorkOrderAuditService(context, locks);
|
|
||||||
|
|
||||||
await audit.LogStatusChangedAsync(2, "Open", "Scheduled", "user-1");
|
await audit.LogStatusChangedAsync(2, "Open", "Scheduled", "user-1");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SeaHaven.DataServices.Implementation;
|
||||||
using SeaHaven.Services.Constants;
|
using SeaHaven.Services.Constants;
|
||||||
using SeaHaven.Services.Implementation;
|
using SeaHaven.Services.Implementation;
|
||||||
|
|
||||||
|
|
@ -19,7 +20,8 @@ public class WorkOrderFieldLockServiceTests
|
||||||
public async Task LockFieldAsync_DuplicateCalls_EnqueuesSingleLock()
|
public async Task LockFieldAsync_DuplicateCalls_EnqueuesSingleLock()
|
||||||
{
|
{
|
||||||
await using var context = CreateContext();
|
await using var context = CreateContext();
|
||||||
var locks = new WorkOrderFieldLockService(context);
|
var lockData = new WorkOrderFieldLockDataService(context);
|
||||||
|
var locks = new WorkOrderFieldLockService(lockData);
|
||||||
|
|
||||||
await locks.LockFieldAsync(1, WorkOrderFieldNames.Description, "user-1");
|
await locks.LockFieldAsync(1, WorkOrderFieldNames.Description, "user-1");
|
||||||
await locks.LockFieldAsync(1, WorkOrderFieldNames.Description, "user-2");
|
await locks.LockFieldAsync(1, WorkOrderFieldNames.Description, "user-2");
|
||||||
|
|
@ -43,7 +45,8 @@ public class WorkOrderFieldLockServiceTests
|
||||||
});
|
});
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
var locks = new WorkOrderFieldLockService(context);
|
var lockData = new WorkOrderFieldLockDataService(context);
|
||||||
|
var locks = new WorkOrderFieldLockService(lockData);
|
||||||
await locks.LockFieldAsync(2, WorkOrderFieldNames.AssignTo, "user-2");
|
await locks.LockFieldAsync(2, WorkOrderFieldNames.AssignTo, "user-2");
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue