From 445cd6846853ca8596d53d2f19ee02affec1c9a7 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 24 Jul 2026 11:24:05 -0300 Subject: [PATCH] refactor(api): complete feature service boundaries --- .../DispatchControllerTests.cs | 77 ++++ .../DispatchServiceTests.cs | 117 ++++++ .../FollowUpControllerTests.cs | 134 +++++++ .../FollowUpServiceTests.cs | 162 +++++++++ .../LocationControllerTests.cs | 104 ++++++ .../LocationServiceTests.cs | 133 +++++++ .../ServiceRegistrationTests.cs | 22 ++ .../SettingsControllerTests.cs | 119 ++++++ .../SettingsServiceTests.cs | 168 +++++++++ .../Controllers/DispatchController.cs | 310 +--------------- .../Controllers/FollowUpController.cs | 222 ++++-------- .../Controllers/LocationController.cs | 97 +++-- .../Controllers/SettingsController.cs | 153 +++----- .../Dto/DispatchReadModels.cs | 169 +++++++++ .../Dto/SettingsReadModels.cs | 8 + .../Implementation/DispatchDataService.cs | 342 ++++++++++++++++++ .../Implementation/FollowUpDataService.cs | 133 +++++++ .../Implementation/LocationDataService.cs | 63 ++++ .../Implementation/SettingsDataService.cs | 125 +++++++ .../Interfaces/IDispatchDataService.cs | 4 + .../Interfaces/IFollowUpDataService.cs | 10 + .../Interfaces/ILocationDataService.cs | 7 + .../Interfaces/ISettingsDataService.cs | 16 + SeaHaven.Services/DTOs/DispatchDTOs.cs | 154 ++++++++ SeaHaven.Services/DTOs/FollowUpDTOs.cs | 65 ++++ SeaHaven.Services/DTOs/LocationDTOs.cs | 26 ++ SeaHaven.Services/DTOs/SettingsDTOs.cs | 98 +++++ .../Implementation/DispatchService.cs | 179 +++++++++ .../Implementation/FollowUpService.cs | 125 +++++++ .../Implementation/LocationService.cs | 64 ++++ .../Implementation/SettingsService.cs | 166 +++++++++ .../Interfaces/IDispatchService.cs | 3 + .../Interfaces/IFollowUpService.cs | 7 + .../Interfaces/ILocationService.cs | 6 + .../Interfaces/ISettingsService.cs | 15 + 35 files changed, 2993 insertions(+), 610 deletions(-) create mode 100644 Api.SeaHavenIndustries.Tests/DispatchControllerTests.cs create mode 100644 Api.SeaHavenIndustries.Tests/DispatchServiceTests.cs create mode 100644 Api.SeaHavenIndustries.Tests/FollowUpControllerTests.cs create mode 100644 Api.SeaHavenIndustries.Tests/FollowUpServiceTests.cs create mode 100644 Api.SeaHavenIndustries.Tests/LocationControllerTests.cs create mode 100644 Api.SeaHavenIndustries.Tests/LocationServiceTests.cs create mode 100644 Api.SeaHavenIndustries.Tests/SettingsControllerTests.cs create mode 100644 Api.SeaHavenIndustries.Tests/SettingsServiceTests.cs create mode 100644 SeaHaven.DataServices/Dto/DispatchReadModels.cs create mode 100644 SeaHaven.DataServices/Dto/SettingsReadModels.cs create mode 100644 SeaHaven.DataServices/Implementation/SettingsDataService.cs create mode 100644 SeaHaven.DataServices/Interfaces/ISettingsDataService.cs create mode 100644 SeaHaven.Services/DTOs/SettingsDTOs.cs create mode 100644 SeaHaven.Services/Implementation/SettingsService.cs create mode 100644 SeaHaven.Services/Interfaces/ISettingsService.cs diff --git a/Api.SeaHavenIndustries.Tests/DispatchControllerTests.cs b/Api.SeaHavenIndustries.Tests/DispatchControllerTests.cs new file mode 100644 index 0000000..90b93ee --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/DispatchControllerTests.cs @@ -0,0 +1,77 @@ +using Api.SeaHavenIndustries.Controllers; +using Data.SeaHavenIndustries; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Interfaces; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public class DispatchControllerTests +{ + private static DispatchController NewController(Mock service) => + new(service.Object); + + private static object Prop(object source, string name) => + source.GetType().GetProperty(name)!.GetValue(source)!; + + [Fact] + public async Task List_ReturnsDataResponseWithPaginationShape() + { + var service = new Mock(); + service.Setup(s => s.GetDispatchListAsync("a", null, null, null, null, "dispatchedAt", "desc", 1, 25, It.IsAny())) + .ReturnsAsync(new DispatchListResultDTO + { + Total = 2, + Page = 1, + PageSize = 25, + Items = new List + { + new() { Id = 1, DispatchNumber = "D1" }, + new() { Id = 2, DispatchNumber = "D2" } + } + }); + + var result = await NewController(service).List("a", null, null, null, null, "dispatchedAt", "desc", 1, 25, CancellationToken.None); + + var ok = result.Should().BeOfType().Subject; + var response = ok.Value.Should().BeOfType().Subject; + response.Status.Should().Be("Success"); + object data = response.Data!; + Prop(data, "total").Should().Be(2); + Prop(data, "page").Should().Be(1); + Prop(data, "pageSize").Should().Be(25); + ((System.Collections.IEnumerable)Prop(data, "items")).Cast().Count().Should().Be(2); + } + + [Fact] + public async Task GetById_WhenMissing_ReturnsNotFound() + { + var service = new Mock(); + service.Setup(s => s.GetDispatchDetailAsync(99, It.IsAny())) + .ReturnsAsync((DispatchDetailResultDTO?)null); + + var result = await NewController(service).GetById(99, CancellationToken.None); + + var notFound = result.Should().BeOfType().Subject; + notFound.Value.Should().BeOfType().Subject.Message.Should().Be("Dispatch not found"); + } + + [Fact] + public async Task GetById_WhenFound_ReturnsDetailAsData() + { + var service = new Mock(); + var detail = new DispatchDetailResultDTO { Id = 5, DispatchNumber = "D5", Status = "Pending" }; + service.Setup(s => s.GetDispatchDetailAsync(5, It.IsAny())) + .ReturnsAsync(detail); + + var result = await NewController(service).GetById(5, CancellationToken.None); + + var ok = result.Should().BeOfType().Subject; + var response = ok.Value.Should().BeOfType().Subject; + response.Status.Should().Be("Success"); + ((object?)response.Data).Should().BeSameAs(detail); + } +} diff --git a/Api.SeaHavenIndustries.Tests/DispatchServiceTests.cs b/Api.SeaHavenIndustries.Tests/DispatchServiceTests.cs new file mode 100644 index 0000000..b4f9654 --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/DispatchServiceTests.cs @@ -0,0 +1,117 @@ +using FluentAssertions; +using Moq; +using SeaHaven.DataServices.Dto; +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; + +public class DispatchServiceTests +{ + private static DispatchService NewService(Mock data) => + new(data.Object, new CreateDispatchValidation(), new UpdateDispatchValidation()); + + [Fact] + public async Task GetDispatchListAsync_MapsDataReadModelToServiceDtoPreservingFields() + { + var data = new Mock(); + var source = new DispatchListResult + { + Total = 1, + Page = 2, + PageSize = 25, + Items = new List + { + new() + { + Id = 7, + DispatchNumber = "DISP-7", + PONumber = "PO-1", + NTEAmount = 200m, + Status = "Pending", + VendorId = 9, + VendorName = "Acme", + WorkOrderId = 3, + PrimaryWOInternalNumber = "WO-3", + PrimaryWOTitle = "Fix AC", + LocationName = "Site A", + WorkOrderCount = 2, + PendingUpliftCount = 1 + } + } + }; + data.Setup(d => d.GetListAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(source); + + var result = await NewService(data).GetDispatchListAsync("a", "Pending", 9, null, null, "ponumber", "asc", 2, 25, CancellationToken.None); + + result.Should().BeOfType(); + result.Total.Should().Be(1); + result.Page.Should().Be(2); + result.PageSize.Should().Be(25); + var row = result.Items.Single(); + row.Id.Should().Be(7); + row.DispatchNumber.Should().Be("DISP-7"); + row.PONumber.Should().Be("PO-1"); + row.NTEAmount.Should().Be(200m); + row.Status.Should().Be("Pending"); + row.VendorId.Should().Be(9); + row.VendorName.Should().Be("Acme"); + row.WorkOrderId.Should().Be(3); + row.PrimaryWOInternalNumber.Should().Be("WO-3"); + row.PrimaryWOTitle.Should().Be("Fix AC"); + row.LocationName.Should().Be("Site A"); + row.WorkOrderCount.Should().Be(2); + row.PendingUpliftCount.Should().Be(1); + } + + [Fact] + public async Task GetDispatchDetailAsync_MapsNestedCollectionsPreservingShape() + { + var data = new Mock(); + var detail = new DispatchDetailDTO + { + Id = 42, + DispatchNumber = "DISP-42", + PONumber = "PO-9", + NTEAmount = 500m, + Status = "Dispatched", + Description = "desc", + WorkOrderId = 11, + EmailSent = true, + Vendor = new DispatchDetailVendorDTO { Id = 1, CompanyName = "VendorCo", Email = "v@x.com" }, + PrimaryLocation = new DispatchPrimaryLocationDTO { Id = 5, Name = "Loc", City = "Austin" }, + WorkOrders = new List { new() { WorkOrderId = 11, Title = "T", Priority = "High" } }, + Checklist = new List { new() { Id = 1, ItemText = "Check", IsCompleted = true, SortOrder = 1 } }, + Signoffs = new List { new() { Id = 1, SignoffType = "Vendor", Name = "J" } }, + Comments = new List { new() { Id = 1, Commenttext = "Hi", UserName = "Sara" } }, + UpliftRequests = new List { new() { Id = 1, RequestedNTE = 600m, CurrentNTE = 500m, Delta = 100m, Status = "Pending" } }, + AuditLog = new List { new() { Id = 1, Action = "Updated", FieldName = "DISP-42:Status" } } + }; + data.Setup(d => d.GetDetailAsync(42, It.IsAny())).ReturnsAsync(detail); + data.Setup(d => d.GetDetailAsync(999, It.IsAny())).ReturnsAsync((DispatchDetailDTO?)null); + + var service = NewService(data); + + var found = await service.GetDispatchDetailAsync(42, CancellationToken.None); + var missing = await service.GetDispatchDetailAsync(999, CancellationToken.None); + + missing.Should().BeNull(); + found.Should().BeOfType(); + found!.Id.Should().Be(42); + found.DispatchNumber.Should().Be("DISP-42"); + found.EmailSent.Should().BeTrue(); + found.Vendor!.CompanyName.Should().Be("VendorCo"); + found.PrimaryLocation!.Name.Should().Be("Loc"); + found.WorkOrders.Single().Title.Should().Be("T"); + found.Checklist.Single().IsCompleted.Should().BeTrue(); + found.Signoffs.Single().Name.Should().Be("J"); + found.Comments.Single().UserName.Should().Be("Sara"); + found.UpliftRequests.Single().Delta.Should().Be(100m); + found.AuditLog.Single().Action.Should().Be("Updated"); + } +} diff --git a/Api.SeaHavenIndustries.Tests/FollowUpControllerTests.cs b/Api.SeaHavenIndustries.Tests/FollowUpControllerTests.cs new file mode 100644 index 0000000..85124ee --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/FollowUpControllerTests.cs @@ -0,0 +1,134 @@ +using System.Security.Claims; +using Api.SeaHavenIndustries.Controllers; +using Api.SeaHavenIndustries.DTOs; +using Data.SeaHavenIndustries; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Interfaces; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public class FollowUpControllerTests +{ + private static FollowUpController NewController(Mock service) + { + var controller = new FollowUpController(service.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.NameIdentifier, "user-1") + }, "Test")) + } + } + }; + return controller; + } + + private static FollowUpController NewControllerNoUser(Mock service) => + new(service.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity()) + } + } + }; + + private static object Prop(object source, string name) => + source.GetType().GetProperty(name)!.GetValue(source)!; + + [Fact] + public async Task GetFollowUps_ReturnsPaginationEnvelopeWithMappedRows() + { + var service = new Mock(); + service.Setup(s => s.GetFollowUpListAsync(1, 12, "", "Open", It.IsAny())) + .ReturnsAsync(new PagedResult + { + Items = new List { new() { Id = 1, Reason = "R", CreatedByName = "Me" } }, + TotalCount = 1, + Page = 1, + PageSize = 12 + }); + + var result = await NewController(service).GetFollowUps(); + + var ok = result.Should().BeOfType().Subject; + var envelope = ok.Value.Should().BeOfType().Subject; + envelope.PageSize.Should().Be(12); + envelope.TotalCount.Should().Be(1); + } + + [Fact] + public async Task GetById_WhenMissing_ReturnsNotFound() + { + var service = new Mock(); + service.Setup(s => s.GetFollowUpDetailAsync(9, It.IsAny())) + .ReturnsAsync((FollowUpDetailDTO?)null); + + var result = await NewController(service).GetById(9, CancellationToken.None); + + result.Should().BeOfType(); + } + + [Fact] + public async Task Create_WhenNoUser_ReturnsUnauthorized() + { + var service = new Mock(); + var result = await NewControllerNoUser(service).Create(new FollowUp_DTO { Reason = "x" }, CancellationToken.None); + + var unauthorized = result.Should().BeOfType().Subject; + unauthorized.Value.Should().BeOfType().Subject.Message.Should().Be("User not authenticated"); + service.Verify(s => s.CreateOpenFollowUpAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Create_WhenAuthenticated_ReturnsCreatedWithId() + { + var service = new Mock(); + service.Setup(s => s.CreateOpenFollowUpAsync(It.IsAny(), "user-1", It.IsAny())) + .ReturnsAsync(42); + + var result = await NewController(service).Create(new FollowUp_DTO { Reason = "x" }, CancellationToken.None); + + var ok = result.Should().BeOfType().Subject; + var response = ok.Value.Should().BeOfType().Subject; + response.Status.Should().Be("200"); + response.Message.Should().Be("Follow Up created"); + ((object?)response.Data).Should().Be(42); + } + + [Fact] + public async Task Complete_WhenMissing_ReturnsNotFound() + { + var service = new Mock(); + service.Setup(s => s.SetFollowUpStatusAsync(3, "Completed", It.IsAny())) + .ReturnsAsync(false); + + var result = await NewController(service).Complete(3, CancellationToken.None); + + result.Should().BeOfType(); + } + + [Fact] + public async Task Delete_WhenMissing_ReturnsNotFoundEnvelope() + { + var service = new Mock(); + service.Setup(s => s.RemoveFollowUpAsync(8, It.IsAny())) + .ReturnsAsync(false); + + var result = await NewController(service).Delete(8, CancellationToken.None); + + var notFound = result.Should().BeOfType().Subject; + notFound.Value.Should().BeOfType().Subject.Message.Should().Be("Follow Up not found"); + } +} diff --git a/Api.SeaHavenIndustries.Tests/FollowUpServiceTests.cs b/Api.SeaHavenIndustries.Tests/FollowUpServiceTests.cs new file mode 100644 index 0000000..d5d7790 --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/FollowUpServiceTests.cs @@ -0,0 +1,162 @@ +using Data.SeaHavenIndustries; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Implementation; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Implementation; +using SeaHaven.Services.Validation; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public class FollowUpServiceTests +{ + private static ApplicationDbContext NewContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + return new ApplicationDbContext(options); + } + + private static FollowUpService NewService(ApplicationDbContext ctx) => + new(new FollowUpDataService(ctx), new CreateFollowUpValidation(), new UpdateFollowUpValidation()); + + private static FollowUps SeedFollowUp(ApplicationDbContext ctx, string reason, string status = "Open") + { + var f = new FollowUps + { + Reason = reason, + ScheduleDate = new DateTime(2026, 8, 1), + ScheduleStartTime = new TimeSpan(9, 0, 0), + Status = status, + createdby = "user-1", + CreatedDate = DateTime.Now + }; + ctx.FollowUps.Add(f); + ctx.SaveChanges(); + return f; + } + + [Fact] + public async Task CreateOpenFollowUpAsync_ForcesOpenStatusAndPersistsFields() + { + using var ctx = NewContext(); + var service = NewService(ctx); + + var id = await service.CreateOpenFollowUpAsync(new FollowUpCreateRequestDTO + { + Reason = "Call back", + ScheduleDate = new DateTime(2026, 9, 1), + ScheduleStartTime = new TimeSpan(10, 30, 0), + EmployeeId = 5, + WorkOrderId = 7, + AccountId = 9, + LocationId = 11 + }, "user-9", CancellationToken.None); + + var entity = ctx.FollowUps.Single(); + entity.Id.Should().Be(id); + entity.Status.Should().Be("Open"); + entity.Reason.Should().Be("Call back"); + entity.createdby.Should().Be("user-9"); + entity.EmployeeId.Should().Be(5); + entity.WorkOrderId.Should().Be(7); + entity.CreatedDate.Should().NotBeNull(); + } + + [Fact] + public async Task GetFollowUpListAsync_DefaultsToOpenAndMapsShape() + { + using var ctx = NewContext(); + SeedFollowUp(ctx, "Open one", "Open"); + SeedFollowUp(ctx, "Done one", "Completed"); + + var page = await NewService(ctx).GetFollowUpListAsync(1, 12, "", null, CancellationToken.None); + + page.Items.Should().HaveCount(1); + page.Items.Single().Reason.Should().Be("Open one"); + page.Items.Single().Status.Should().Be("Open"); + page.PageSize.Should().Be(12); + } + + [Fact] + public async Task GetFollowUpDetailAsync_MapsRelatedNamesAndResolvesCreator() + { + using var ctx = NewContext(); + var employee = new Employee { FirstName = "Sara", LastName = "Lee" }; + ctx.Employees.Add(employee); + ctx.SaveChanges(); + + var account = new Accounts { Name = "Acme" }; + ctx.Accounts.Add(account); + ctx.SaveChanges(); + + var user = new ApplicationUser { Id = "user-1", FirstName = "Creator", LastName = "One", UserName = "cone" }; + var fu = new FollowUps + { + Reason = "Detail", + ScheduleDate = new DateTime(2026, 8, 2), + ScheduleStartTime = new TimeSpan(8, 0, 0), + Status = "Open", + EmployeeId = employee.Id, + AccountId = account.Id, + createdby = "user-1", + CreatedDate = DateTime.Now + }; + ctx.Users.Add(user); + ctx.FollowUps.Add(fu); + ctx.SaveChanges(); + + var detail = await NewService(ctx).GetFollowUpDetailAsync(fu.Id, CancellationToken.None); + + detail.Should().NotBeNull(); + detail!.AssignedTo.Should().Be("Sara Lee"); + detail.AccountName.Should().Be("Acme"); + detail.CreatedById.Should().Be("user-1"); + detail.CreatedByName.Should().Be("Creator One"); + } + + [Fact] + public async Task UpdateFollowUpFieldsAsync_UpdatesAndThrowsWhenMissing() + { + using var ctx = NewContext(); + var existing = SeedFollowUp(ctx, "Old reason"); + + await NewService(ctx).UpdateFollowUpFieldsAsync(existing.Id, new FollowUpUpdateRequestDTO + { + Id = existing.Id, + Reason = "New reason", + ScheduleDate = new DateTime(2026, 10, 1), + ScheduleStartTime = new TimeSpan(11, 0, 0) + }, CancellationToken.None); + + var row = ctx.FollowUps.Single(); + row.Reason.Should().Be("New reason"); + + var act = () => NewService(ctx).UpdateFollowUpFieldsAsync(existing.Id + 999, new FollowUpUpdateRequestDTO { Id = existing.Id + 999 }, CancellationToken.None); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task SetFollowUpStatusAsync_TogglesStatusAndReturnsFalseWhenMissing() + { + using var ctx = NewContext(); + var existing = SeedFollowUp(ctx, "To complete"); + + (await NewService(ctx).SetFollowUpStatusAsync(existing.Id, "Completed", CancellationToken.None)).Should().BeTrue(); + ctx.FollowUps.Single().Status.Should().Be("Completed"); + (await NewService(ctx).SetFollowUpStatusAsync(existing.Id + 999, "Completed", CancellationToken.None)).Should().BeFalse(); + } + + [Fact] + public async Task RemoveFollowUpAsync_RemovesAndReturnsFalseWhenMissing() + { + using var ctx = NewContext(); + var existing = SeedFollowUp(ctx, "Gone"); + + (await NewService(ctx).RemoveFollowUpAsync(existing.Id, CancellationToken.None)).Should().BeTrue(); + (await NewService(ctx).RemoveFollowUpAsync(existing.Id, CancellationToken.None)).Should().BeFalse(); + ctx.FollowUps.Should().BeEmpty(); + } +} diff --git a/Api.SeaHavenIndustries.Tests/LocationControllerTests.cs b/Api.SeaHavenIndustries.Tests/LocationControllerTests.cs new file mode 100644 index 0000000..6a326c5 --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/LocationControllerTests.cs @@ -0,0 +1,104 @@ +using Api.SeaHavenIndustries.Controllers; +using Api.SeaHavenIndustries.DTOs; +using Data.SeaHavenIndustries; +using FluentAssertions; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Interfaces; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public class LocationControllerTests +{ + private static LocationController NewController(Mock service) => + new(service.Object); + + private static object Prop(object source, string name) => + source.GetType().GetProperty(name)!.GetValue(source)!; + + [Fact] + public async Task GetLocationList_ReturnsPaginationEnvelopeWithServiceData() + { + var service = new Mock(); + service.Setup(s => s.GetLocationListPagedAsync(1, 10, "a", It.IsAny())) + .ReturnsAsync(new PagedResult + { + Items = new List { new() { Id = 1, Name = "Site" } }, + TotalCount = 1, + Page = 1, + PageSize = 10 + }); + + var result = await NewController(service).GetLocationList("a", 1, 10, CancellationToken.None); + + var ok = result.Should().BeOfType().Subject; + var envelope = ok.Value.Should().BeOfType().Subject; + envelope.PageNumber.Should().Be(1); + envelope.PageSize.Should().Be(10); + envelope.TotalCount.Should().Be(1); + envelope.TotalPages.Should().Be(1); + } + + [Fact] + public async Task GetLocationById_WhenMissing_ReturnsNotFoundAndDoesNotLeakEntity() + { + var service = new Mock(); + service.Setup(s => s.GetLocationDetailAsync(5, It.IsAny())) + .ReturnsAsync((LocationDTO?)null); + + var result = await NewController(service).GetLocationById(5, CancellationToken.None); + + var notFound = result.Should().BeOfType().Subject; + var response = notFound.Value.Should().BeOfType().Subject; + response.Message.Should().Be("Location not found"); + } + + [Fact] + public async Task GetLocationById_WhenFound_MapsServiceDtoToAliasedShape() + { + var service = new Mock(); + service.Setup(s => s.GetLocationDetailAsync(7, It.IsAny())) + .ReturnsAsync(new LocationDTO { Id = 7, Name = "N", Address1 = "A", Zip = "Z", PhoneNumber = "P", Email = "E", Status = "Active" }); + + var result = await NewController(service).GetLocationById(7, CancellationToken.None); + + var ok = result.Should().BeOfType().Subject; + var body = ok.Value!; + Prop(body, "Id").Should().Be(7); + Prop(body, "Name").Should().Be("N"); + Prop(body, "Address").Should().Be("A"); + Prop(body, "ZipCode").Should().Be("Z"); + Prop(body, "Phone").Should().Be("P"); + Prop(body, "ContactEmail").Should().Be("E"); + Prop(body, "Status").Should().Be("Active"); + } + + [Fact] + public async Task AddLocation_WhenServiceSucceeds_ReturnsCreatedResponse() + { + var service = new Mock(); + var result = await NewController(service).AddLocation(new Location_DTO { Name = "X" }, CancellationToken.None); + + var ok = result.Should().BeOfType().Subject; + var response = ok.Value.Should().BeOfType().Subject; + response.Status.Should().Be("200"); + response.Message.Should().Be("Location Created Successfully"); + service.Verify(s => s.CreateLocationFromRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task EditLocation_WhenMissing_ReturnsNotFound() + { + var service = new Mock(); + service.Setup(s => s.UpdateLocationFromRequestAsync(99, It.IsAny(), It.IsAny())) + .ThrowsAsync(new KeyNotFoundException()); + + var result = await NewController(service).EditLocation(99, new EditLocation_DTO { Name = "X" }, CancellationToken.None); + + var notFound = result.Should().BeOfType().Subject; + notFound.Value.Should().BeOfType().Subject.Message.Should().Be("Location not found"); + } +} diff --git a/Api.SeaHavenIndustries.Tests/LocationServiceTests.cs b/Api.SeaHavenIndustries.Tests/LocationServiceTests.cs new file mode 100644 index 0000000..5f3abe2 --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/LocationServiceTests.cs @@ -0,0 +1,133 @@ +using Data.SeaHavenIndustries; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Implementation; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Implementation; +using SeaHaven.Services.Validation; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public class LocationServiceTests +{ + private static ApplicationDbContext NewContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + return new ApplicationDbContext(options); + } + + private static LocationService NewService(ApplicationDbContext ctx) => + new(new LocationDataService(ctx), new CreateLocationValidation(), new UpdateLocationValidation()); + + private static Locations SeedLocation(ApplicationDbContext ctx, string name, string? city = null, string? status = "Active") + { + var loc = new Locations { Name = name, City = city, Status = status, CreatedDate = DateTime.Now }; + ctx.Locations.Add(loc); + ctx.SaveChanges(); + return loc; + } + + [Fact] + public async Task CreateLocationFromRequestAsync_PersistsMappedFields() + { + using var ctx = NewContext(); + var service = NewService(ctx); + + await service.CreateLocationFromRequestAsync(new LocationCreateRequestDTO + { + Name = "Warehouse", + Title = "Main WH", + Address = "1 Depot Rd", + City = "Austin", + State = "TX", + ZipCode = "73301", + Phone = "555-1000", + ContactEmail = "wh@example.com", + Status = "Active" + }, CancellationToken.None); + + var entity = ctx.Locations.Single(); + entity.Name.Should().Be("Warehouse"); + entity.Title.Should().Be("Main WH"); + entity.Address1.Should().Be("1 Depot Rd"); + entity.City.Should().Be("Austin"); + entity.State.Should().Be("TX"); + entity.Zip.Should().Be("73301"); + entity.PhoneNumber.Should().Be("555-1000"); + entity.Email.Should().Be("wh@example.com"); + entity.Status.Should().Be("Active"); + } + + [Fact] + public async Task GetLocationListPagedAsync_FiltersBySearchAndMapsShape() + { + using var ctx = NewContext(); + SeedLocation(ctx, "Alpha Site", "Austin"); + SeedLocation(ctx, "Beta Site", "Dallas"); + SeedLocation(ctx, "Gamma Yard", "Austin"); + + var page = await NewService(ctx).GetLocationListPagedAsync(1, 10, "Austin", CancellationToken.None); + + page.Items.Should().HaveCount(2); + page.TotalCount.Should().Be(2); + page.Items.All(l => l.City == "Austin").Should().BeTrue(); + page.Items.Select(l => l.Name).Should().NotBeNull(); + } + + [Fact] + public async Task GetLocationDetailAsync_ReturnsMappedDtoOrNull() + { + using var ctx = NewContext(); + var existing = SeedLocation(ctx, "Depot", "Houston"); + + var service = NewService(ctx); + var found = await service.GetLocationDetailAsync(existing.Id, CancellationToken.None); + var missing = await service.GetLocationDetailAsync(existing.Id + 999, CancellationToken.None); + + missing.Should().BeNull(); + found.Should().NotBeNull(); + found!.Name.Should().Be("Depot"); + found.City.Should().Be("Houston"); + } + + [Fact] + public async Task UpdateLocationFromRequestAsync_UpdatesFieldsAndThrowsWhenMissing() + { + using var ctx = NewContext(); + var existing = SeedLocation(ctx, "Old", "Round Rock"); + + await NewService(ctx).UpdateLocationFromRequestAsync(existing.Id, new LocationUpdateRequestDTO + { + Name = "New", + Address = "9 New St", + City = "Plano", + Status = "Inactive" + }, CancellationToken.None); + + var row = ctx.Locations.Single(); + row.Name.Should().Be("New"); + row.Address1.Should().Be("9 New St"); + row.City.Should().Be("Plano"); + row.Status.Should().Be("Inactive"); + + var act = () => NewService(ctx).UpdateLocationFromRequestAsync(existing.Id + 999, new LocationUpdateRequestDTO { Name = "X" }, CancellationToken.None); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task DeleteLocationByIdAsync_RemovesAndReturnsFalseWhenMissing() + { + using var ctx = NewContext(); + var existing = SeedLocation(ctx, "Gone", "Cedar Park"); + + var removed = await NewService(ctx).DeleteLocationByIdAsync(existing.Id, CancellationToken.None); + var again = await NewService(ctx).DeleteLocationByIdAsync(existing.Id, CancellationToken.None); + + removed.Should().BeTrue(); + again.Should().BeFalse(); + ctx.Locations.Should().BeEmpty(); + } +} diff --git a/Api.SeaHavenIndustries.Tests/ServiceRegistrationTests.cs b/Api.SeaHavenIndustries.Tests/ServiceRegistrationTests.cs index 2127983..bdbd896 100644 --- a/Api.SeaHavenIndustries.Tests/ServiceRegistrationTests.cs +++ b/Api.SeaHavenIndustries.Tests/ServiceRegistrationTests.cs @@ -54,4 +54,26 @@ public class ServiceRegistrationTests provider.GetRequiredService().Should().BeOfType(); provider.GetRequiredService().Should().BeOfType(); } + + [Fact] + public void ConventionDi_RegistersFollowUpLocationDispatchSettingsServices() + { + using var provider = BuildProvider(); + + provider.GetRequiredService().Should().BeOfType(); + provider.GetRequiredService().Should().BeOfType(); + provider.GetRequiredService().Should().BeOfType(); + provider.GetRequiredService().Should().BeOfType(); + } + + [Fact] + public void ConventionDi_RegistersFollowUpLocationDispatchSettingsDataServices() + { + using var provider = BuildProvider(); + + provider.GetRequiredService().Should().BeOfType(); + provider.GetRequiredService().Should().BeOfType(); + provider.GetRequiredService().Should().BeOfType(); + provider.GetRequiredService().Should().BeOfType(); + } } diff --git a/Api.SeaHavenIndustries.Tests/SettingsControllerTests.cs b/Api.SeaHavenIndustries.Tests/SettingsControllerTests.cs new file mode 100644 index 0000000..aad00cd --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/SettingsControllerTests.cs @@ -0,0 +1,119 @@ +using Api.SeaHavenIndustries.Controllers; +using Api.SeaHavenIndustries.DTOs; +using Api.SeaHavenIndustries.Helper.ViewModel; +using Data.SeaHavenIndustries; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Interfaces; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public class SettingsControllerTests +{ + private static SettingsController NewController(Mock service) => + new(service.Object); + + [Fact] + public async Task GetCategoryList_ReturnsPaginationEnvelopeWithServiceItems() + { + var service = new Mock(); + service.Setup(s => s.GetCategoryListAsync(1, 10, It.IsAny())) + .ReturnsAsync((new List { new() { Id = 1, Name = "A" } }, 1)); + + var result = await NewController(service).GetCategoryList(1, 10, CancellationToken.None); + + var ok = result.Should().BeOfType().Subject; + var envelope = ok.Value.Should().BeOfType().Subject; + envelope.TotalCount.Should().Be(1); + envelope.TotalPages.Should().Be(1); + } + + [Fact] + public async Task GetTemplates_ReturnsTypedServiceDtosInEnvelope() + { + var service = new Mock(); + service.Setup(s => s.GetTemplatesAsync(1, 10, It.IsAny())) + .ReturnsAsync((new List { new() { Id = 1, WorkerOrderTitle = "T" } }, 1)); + + var result = await NewController(service).GetTemplates(1, 10, CancellationToken.None); + + var ok = result.Should().BeOfType().Subject; + var envelope = ok.Value.Should().BeOfType().Subject; + envelope.TotalCount.Should().Be(1); + } + + [Fact] + public async Task AddCategory_WhenServiceSucceeds_ReturnsSaved() + { + var service = new Mock(); + var result = await NewController(service).AddCategory(new Category_DTO { Name = "X" }, CancellationToken.None); + + var ok = result.Should().BeOfType().Subject; + var response = ok.Value.Should().BeOfType().Subject; + response.Status.Should().Be("200"); + response.Message.Should().Be("Successfully Saved Data"); + service.Verify(s => s.AddCategoryAsync("X", It.IsAny()), Times.Once); + } + + [Fact] + public async Task EditCategory_WhenMissing_ReturnsBadRequest() + { + var service = new Mock(); + service.Setup(s => s.UpdateCategoryAsync(9, "X", It.IsAny())) + .ReturnsAsync(false); + + var result = await NewController(service).EditCategory(new EditCategory_DTO { Id = 9, Name = "X" }, CancellationToken.None); + + var bad = result.Should().BeOfType().Subject; + bad.Value.Should().BeOfType().Subject.Message.Should().Be("Category not found"); + } + + [Fact] + public async Task DeleteCategory_WhenMissing_ReturnsIdNotMatched() + { + var service = new Mock(); + service.Setup(s => s.DeleteCategoryAsync(9, It.IsAny())) + .ReturnsAsync(false); + + var result = await NewController(service).DeleteCategory(9, CancellationToken.None); + + var bad = result.Should().BeOfType().Subject; + bad.Value.Should().BeOfType().Subject.Message.Should().Be("ID not matched!"); + } + + [Fact] + public async Task GetUsers_MapsServiceUsersToUserRoleViewModel() + { + var service = new Mock(); + service.Setup(s => s.GetUsersAsync("", 1, 10, It.IsAny())) + .ReturnsAsync((new List + { + new() { Id = "u1", UserName = "Sara", Email = "s@x.com", RoleName = "User", Status = "Active", Date = "2026-01-01" } + }, 1)); + + var result = await NewController(service).GetUsers("", 1, 10, CancellationToken.None); + + var ok = result.Should().BeOfType().Subject; + var envelope = ok.Value.Should().BeOfType().Subject; + object data = envelope.Data!; + var rows = ((System.Collections.IEnumerable)data).Cast().ToList(); + rows.Should().ContainSingle(u => u.UserName == "Sara" && u.RoleName == "User" && u.Email == "s@x.com"); + } + + [Fact] + public async Task GetRoles_MapsServiceRolesToRolesDto() + { + var service = new Mock(); + service.Setup(s => s.GetRolesAsync(It.IsAny())) + .ReturnsAsync(new List { new() { Name = "Admin", Description = "Full Access" } }); + + var result = await NewController(service).GetRoles(CancellationToken.None); + + var ok = result.Should().BeOfType().Subject; + var roles = ((System.Collections.IEnumerable)ok.Value!).Cast().ToList(); + roles.Should().ContainSingle(r => r.Name == "Admin" && r.Description == "Full Access"); + } +} diff --git a/Api.SeaHavenIndustries.Tests/SettingsServiceTests.cs b/Api.SeaHavenIndustries.Tests/SettingsServiceTests.cs new file mode 100644 index 0000000..b7c50a1 --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/SettingsServiceTests.cs @@ -0,0 +1,168 @@ +using Data.SeaHavenIndustries; +using FluentAssertions; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SeaHaven.DataServices.Implementation; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Implementation; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public class SettingsServiceTests +{ + private static ApplicationDbContext NewContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + return new ApplicationDbContext(options); + } + + private static SettingsService NewService(ApplicationDbContext ctx) => + new(new SettingsDataService(ctx), null!); + + private static (ApplicationDbContext ctx, UserManager userManager, RoleManager roleManager) NewIdentityContext() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddDbContext(o => o.UseInMemoryDatabase(Guid.NewGuid().ToString())); + services.AddIdentity().AddEntityFrameworkStores(); + var provider = services.BuildServiceProvider(); + var scope = provider.CreateScope(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + var roleManager = scope.ServiceProvider.GetRequiredService>(); + return (ctx, userManager, roleManager); + } + + [Fact] + public async Task AddCategoryAsync_PersistsName() + { + using var ctx = NewContext(); + await NewService(ctx).AddCategoryAsync("Plumbing", CancellationToken.None); + ctx.Categories.Single().Name.Should().Be("Plumbing"); + } + + [Fact] + public async Task UpdateCategoryAsync_UpdatesNameAndReturnsFalseWhenMissing() + { + using var ctx = NewContext(); + ctx.Categories.Add(new Category { Name = "Old" }); + ctx.SaveChanges(); + + var service = NewService(ctx); + (await service.UpdateCategoryAsync(1, "New", CancellationToken.None)).Should().BeTrue(); + ctx.Categories.Single().Name.Should().Be("New"); + (await service.UpdateCategoryAsync(999, "X", CancellationToken.None)).Should().BeFalse(); + } + + [Fact] + public async Task DeleteCategoryAsync_CascadesTemplatesAndWorkOrderCategories() + { + using var ctx = NewContext(); + ctx.Categories.Add(new Category { Id = 1, Name = "C1" }); + ctx.Templates.Add(new Template { CategoryId = 1, WorkerOrderTitle = "T" }); + ctx.workOrderCategories.Add(new WorkOrderCategories { CategoryId = 1 }); + ctx.SaveChanges(); + + var service = NewService(ctx); + (await service.DeleteCategoryAsync(1, CancellationToken.None)).Should().BeTrue(); + (await service.DeleteCategoryAsync(1, CancellationToken.None)).Should().BeFalse(); + + ctx.Categories.Should().BeEmpty(); + ctx.Templates.Should().BeEmpty(); + ctx.workOrderCategories.Should().BeEmpty(); + } + + [Fact] + public async Task GetCategoryListAsync_MapsToServiceDtoAndPages() + { + using var ctx = NewContext(); + ctx.Categories.Add(new Category { Name = "A" }); + ctx.Categories.Add(new Category { Name = "B" }); + ctx.SaveChanges(); + + var (items, total) = await NewService(ctx).GetCategoryListAsync(1, 10, CancellationToken.None); + + total.Should().Be(2); + items.Should().OnlyContain(c => c is SettingsCategoryDTO); + items.Select(c => c.Name).Should().Contain(new[] { "A", "B" }); + } + + [Fact] + public async Task GetTemplatesAsync_MapsWorkOrderScalarsToTypedDto() + { + using var ctx = NewContext(); + var wo = new WorkOrder + { + istemplate = true, + InternalWONumber = "WO-100", + WorkerOrderTitle = "Template A", + Status = "Active", + Priority = "High", + Trade = "HVAC", + VendorNTE = 1500m, + Customer = "Cust" + }; + ctx.workOrders.Add(wo); + ctx.SaveChanges(); + + var (items, total) = await NewService(ctx).GetTemplatesAsync(1, 10, CancellationToken.None); + + total.Should().Be(1); + var dto = items.Single(); + dto.Should().BeOfType(); + dto.InternalWONumber.Should().Be("WO-100"); + dto.WorkerOrderTitle.Should().Be("Template A"); + dto.Status.Should().Be("Active"); + dto.Priority.Should().Be("High"); + dto.Trade.Should().Be("HVAC"); + dto.VendorNTE.Should().Be(1500m); + dto.Customer.Should().Be("Cust"); + dto.istemplate.Should().BeTrue(); + } + + [Fact] + public async Task GetUsersAsync_ExcludesAdminsAndResolvesRole() + { + var (ctx, userManager, roleManager) = NewIdentityContext(); + var t1 = roleManager.CreateAsync(new IdentityRole("Admin")); + var t2 = roleManager.CreateAsync(new IdentityRole("User")); + await Task.WhenAll(t1, t2); + + var admin = new ApplicationUser { UserName = "admin1", Email = "a@x.com", FirstName = "Boss" }; + var worker = new ApplicationUser { UserName = "worker1", Email = "w@x.com", FirstName = "Hand" }; + await userManager.CreateAsync(admin); + await userManager.CreateAsync(worker); + await userManager.AddToRoleAsync(admin, "Admin"); + await userManager.AddToRoleAsync(worker, "User"); + + ctx.SaveChanges(); + + var service = new SettingsService(new SettingsDataService(ctx), userManager); + var (users, total) = await service.GetUsersAsync("", 1, 10, CancellationToken.None); + + total.Should().BeGreaterThanOrEqualTo(2); + users.Should().NotContain(u => u.RoleName == "Admin"); + users.Should().Contain(u => u.UserName == "Hand" && u.RoleName == "User"); + } + + [Fact] + public async Task GetRolesAsync_MapsKnownRoleNamesToDescriptions() + { + using var ctx = NewContext(); + ctx.Roles.Add(new IdentityRole("Admin")); + ctx.Roles.Add(new IdentityRole("Manager")); + ctx.Roles.Add(new IdentityRole("Unknown")); + ctx.SaveChanges(); + + var roles = await NewService(ctx).GetRolesAsync(CancellationToken.None); + + var byName = roles.ToDictionary(r => r.Name!, r => r.Description); + byName["Admin"].Should().Be("Full Access"); + byName["Manager"].Should().Be("Full access accept quotes"); + roles.Should().NotContain(r => r.Name == "Unknown"); + } +} diff --git a/Api.SeaHavenIndustries/Controllers/DispatchController.cs b/Api.SeaHavenIndustries/Controllers/DispatchController.cs index 530be20..f114a4a 100644 --- a/Api.SeaHavenIndustries/Controllers/DispatchController.cs +++ b/Api.SeaHavenIndustries/Controllers/DispatchController.cs @@ -1,9 +1,7 @@ using Data.SeaHavenIndustries; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using SeaHaven.Services.Interfaces; -using SeaHaven.Services.DTOs; namespace Api.SeaHavenIndustries.Controllers { @@ -14,12 +12,10 @@ namespace Api.SeaHavenIndustries.Controllers public class DispatchController : Controller { private readonly IDispatchService _dispatchService; - private readonly ApplicationDbContext _db; // Keep for complex queries - public DispatchController(IDispatchService dispatchService, ApplicationDbContext db) + public DispatchController(IDispatchService dispatchService) { _dispatchService = dispatchService; - _db = db; } [HttpGet] @@ -32,317 +28,29 @@ namespace Api.SeaHavenIndustries.Controllers [FromQuery] string? sortBy = "dispatchedAt", [FromQuery] string? sortDir = "desc", [FromQuery] int page = 1, - [FromQuery] int pageSize = 25) + [FromQuery] int pageSize = 25, + CancellationToken cancellationToken = default) { - var q = _db.Dispatches - .Include(d => d.Vendor) - .Include(d => d.WorkOrder).ThenInclude(w => w!.Locations) - .Where(d => d.IsDeleted == null || d.IsDeleted == false); - - if (!string.IsNullOrWhiteSpace(search)) - { - var s = search.ToLower(); - q = q.Where(d => - (d.DispatchNumber ?? "").ToLower().Contains(s) || - (d.PONumber ?? "").ToLower().Contains(s) || - (d.Vendor != null && (d.Vendor.CompanyName ?? "").ToLower().Contains(s)) || - (d.WorkOrder != null && (d.WorkOrder.InternalWONumber ?? "").ToLower().Contains(s)) || - (d.WorkOrder != null && (d.WorkOrder.WorkerOrderTitle ?? "").ToLower().Contains(s)) || - (d.WorkOrder != null && d.WorkOrder.Locations != null && (d.WorkOrder.Locations.Name ?? "").ToLower().Contains(s)) - ); - } - - if (!string.IsNullOrWhiteSpace(status)) - q = q.Where(d => d.Status == status); - - if (vendorId.HasValue) - q = q.Where(d => d.VendorId == vendorId.Value); - - if (dateFrom.HasValue) - q = q.Where(d => d.DispatchedAt >= dateFrom.Value); - - if (dateTo.HasValue) - { - var end = dateTo.Value.Date.AddDays(1); - q = q.Where(d => d.DispatchedAt < end); - } - - var total = await q.CountAsync(); - - var dir = (sortDir ?? "desc").ToLowerInvariant() == "asc"; - switch ((sortBy ?? "").ToLowerInvariant()) - { - case "ponumber": - q = dir ? q.OrderBy(d => d.PONumber) : q.OrderByDescending(d => d.PONumber); - break; - case "dispatchnumber": - q = dir ? q.OrderBy(d => d.DispatchNumber) : q.OrderByDescending(d => d.DispatchNumber); - break; - case "vendor": - q = dir ? q.OrderBy(d => d.Vendor!.CompanyName) : q.OrderByDescending(d => d.Vendor!.CompanyName); - break; - case "nte": - case "nteamount": - q = dir ? q.OrderBy(d => d.NTEAmount) : q.OrderByDescending(d => d.NTEAmount); - break; - case "status": - q = dir ? q.OrderBy(d => d.Status) : q.OrderByDescending(d => d.Status); - break; - case "scheduleddate": - q = dir ? q.OrderBy(d => d.ScheduledDate) : q.OrderByDescending(d => d.ScheduledDate); - break; - case "completeddate": - q = dir ? q.OrderBy(d => d.CompletedDate) : q.OrderByDescending(d => d.CompletedDate); - break; - default: - q = dir ? q.OrderBy(d => d.DispatchedAt) : q.OrderByDescending(d => d.DispatchedAt); - break; - } - - var rows = await q - .Skip((Math.Max(page, 1) - 1) * pageSize) - .Take(pageSize) - .Select(d => new - { - d.Id, - d.DispatchNumber, - d.PONumber, - d.NTEAmount, - d.Status, - d.DispatchedAt, - d.ScheduledDate, - d.CompletedDate, - d.AcknowledgedAt, - d.VerifiedAt, - VendorId = d.VendorId, - VendorName = d.Vendor != null ? d.Vendor.CompanyName : null, - WorkOrderId = d.WorkOrderId, - PrimaryWOInternalNumber = d.WorkOrder != null ? d.WorkOrder.InternalWONumber : null, - PrimaryWOTitle = d.WorkOrder != null ? d.WorkOrder.WorkerOrderTitle : null, - LocationName = d.WorkOrder != null && d.WorkOrder.Locations != null ? d.WorkOrder.Locations.Name : null, - WorkOrderCount = _db.DispatchWorkOrders.Count(dwo => dwo.DispatchId == d.Id), - PendingUpliftCount = _db.DispatchUpliftRequests.Count(u => u.DispatchId == d.Id && u.Status == "Pending" && (u.IsDeleted == null || u.IsDeleted == false)) - }) - .ToListAsync(); + var result = await _dispatchService.GetDispatchListAsync( + search, status, vendorId, dateFrom, dateTo, sortBy, sortDir, page, pageSize, cancellationToken); return Ok(new DataResponse { Status = "Success", - Data = new { total, page, pageSize, items = rows } + Data = new { total = result.Total, page = result.Page, pageSize = result.PageSize, items = result.Items } }); } [HttpGet("{id:int}")] - public async Task GetById(int id) + public async Task GetById(int id, CancellationToken cancellationToken) { - var dispatch = await _db.Dispatches - .Include(d => d.Vendor) - .Include(d => d.WorkOrder).ThenInclude(w => w!.Locations) - .FirstOrDefaultAsync(d => d.Id == id); + var dispatch = await _dispatchService.GetDispatchDetailAsync(id, cancellationToken); if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" }); - var linkedWOs = await _db.DispatchWorkOrders - .Where(dwo => dwo.DispatchId == id) - .Select(dwo => new - { - dwo.WorkOrderId, - InternalWONumber = dwo.WorkOrder != null ? dwo.WorkOrder.InternalWONumber : null, - Title = dwo.WorkOrder != null ? dwo.WorkOrder.WorkerOrderTitle : null, - Priority = dwo.WorkOrder != null ? dwo.WorkOrder.Priority : null, - Status = dwo.WorkOrder != null ? dwo.WorkOrder.Status : null, - LocationName = dwo.WorkOrder != null && dwo.WorkOrder.Locations != null ? dwo.WorkOrder.Locations.Name : null, - Trade = dwo.WorkOrder != null ? dwo.WorkOrder.Trade : null, - SubTrade = dwo.WorkOrder != null ? dwo.WorkOrder.SubTrade : null, - Problem = dwo.WorkOrder != null ? dwo.WorkOrder.Problem : null - }).ToListAsync(); - - if (linkedWOs.Count == 0 && dispatch.WorkOrderId.HasValue) - { - var wo = await _db.workOrders - .Include(w => w.Locations) - .FirstOrDefaultAsync(w => w.Id == dispatch.WorkOrderId.Value); - if (wo != null) - { - linkedWOs = new[] { new - { - WorkOrderId = wo.Id, - InternalWONumber = wo.InternalWONumber, - Title = wo.WorkerOrderTitle, - Priority = wo.Priority, - Status = wo.Status, - LocationName = wo.Locations != null ? wo.Locations.Name : null, - Trade = wo.Trade, - SubTrade = wo.SubTrade, - Problem = wo.Problem - }}.ToList(); - } - } - - var checklist = await _db.DispatchChecklistItems - .Where(c => c.DispatchId == id && (c.IsDeleted == null || c.IsDeleted == false)) - .OrderBy(c => c.SortOrder) - .Select(c => new { c.Id, c.ItemText, c.IsCompleted, c.CompletedBy, c.CompletedAt, c.SortOrder, c.WorkOrderId }) - .ToListAsync(); - - var signoffs = await _db.DispatchSignoffs - .Where(s => s.DispatchId == id && (s.IsDeleted == null || s.IsDeleted == false)) - .Select(s => new { s.Id, s.SignoffType, s.Name, s.Signature, s.SignatureMethod, s.SignedAt }) - .ToListAsync(); - - var rawComments = await (from c in _db.Comments - where c.DispatchId == id && (c.IsDeleted == null || c.IsDeleted == false) - join u in _db.Users on c.UserId equals u.Id into users - from u in users.DefaultIfEmpty() - orderby c.CreatedDate - select new - { - c.Id, - c.Commenttext, - c.Commenter, - c.CommentType, - c.RecordType, - c.CreatedDate, - c.UserId, - UserFirstName = u != null ? u.FirstName : null, - UserLastName = u != null ? u.LastName : null - }) - .ToListAsync(); - - var comments = rawComments.Select(c => new - { - c.Id, - c.Commenttext, - c.Commenter, - c.CommentType, - c.RecordType, - c.CreatedDate, - UserName = string.Join(" ", new[] { c.UserFirstName, c.UserLastName } - .Where(s => !string.IsNullOrWhiteSpace(s))).Trim() - }).ToList(); - - var rawUplifts = await (from u in _db.DispatchUpliftRequests - where u.DispatchId == id && (u.IsDeleted == null || u.IsDeleted == false) - join dec in _db.Users on u.DecidedByUserId equals dec.Id into decs - from dec in decs.DefaultIfEmpty() - orderby u.CreatedDate descending - select new - { - u.Id, - u.CurrentNTE, - u.RequestedNTE, - u.VendorReason, - u.Status, - u.RequiredTier, - u.RequestedByVendorName, - RequestedAt = u.CreatedDate, - u.DecidedAt, - u.DecisionNote, - DecidedByFirstName = dec != null ? dec.FirstName : null, - DecidedByLastName = dec != null ? dec.LastName : null - }) - .ToListAsync(); - - var upliftRequests = rawUplifts.Select(u => new - { - u.Id, - u.CurrentNTE, - u.RequestedNTE, - Delta = u.RequestedNTE - (u.CurrentNTE ?? 0m), - u.VendorReason, - u.Status, - u.RequiredTier, - u.RequestedByVendorName, - u.RequestedAt, - u.DecidedAt, - u.DecisionNote, - DecidedByName = string.Join(" ", new[] { u.DecidedByFirstName, u.DecidedByLastName } - .Where(s => !string.IsNullOrWhiteSpace(s))).Trim() - }).ToList(); - - var dispatchNumber = dispatch.DispatchNumber ?? ""; - var workOrderIds = linkedWOs.Select(l => l.WorkOrderId).Where(wid => wid > 0).ToList(); - - var rawAudit = await (from a in _db.WorkOrderAuditLogs - where workOrderIds.Contains(a.WorkOrderId) - && a.FieldName != null - && a.FieldName.Contains(dispatchNumber) - join u in _db.Users on a.UserId equals u.Id into users - from u in users.DefaultIfEmpty() - orderby a.CreatedAt descending - select new - { - a.Id, - a.Action, - a.FieldName, - a.OldValue, - a.NewValue, - a.CreatedAt, - a.UserId, - UserFirstName = u != null ? u.FirstName : null, - UserLastName = u != null ? u.LastName : null - }) - .ToListAsync(); - - var auditLog = rawAudit.Select(a => new - { - Id = a.Id, - Action = a.Action, - FieldName = a.FieldName, - OldValue = a.OldValue, - NewValue = a.NewValue, - CreatedAt = a.CreatedAt, - UserName = string.Join(" ", new[] { a.UserFirstName, a.UserLastName } - .Where(s => !string.IsNullOrWhiteSpace(s))).Trim() - }).ToList(); - return Ok(new DataResponse { Status = "Success", - Data = new - { - dispatch.Id, - dispatch.DispatchNumber, - dispatch.PONumber, - dispatch.NTEAmount, - dispatch.Description, - dispatch.Status, - dispatch.ScheduledDate, - dispatch.CompletedDate, - dispatch.DispatchedAt, - dispatch.AcknowledgedAt, - dispatch.VerifiedAt, - dispatch.VerifiedBy, - dispatch.EmailSent, - dispatch.WorkOrderId, - Vendor = dispatch.Vendor == null ? null : new - { - dispatch.Vendor.Id, - dispatch.Vendor.CompanyName, - dispatch.Vendor.ContactName, - dispatch.Vendor.Email, - dispatch.Vendor.Phone, - dispatch.Vendor.Address, - dispatch.Vendor.City, - dispatch.Vendor.State, - dispatch.Vendor.Zip, - dispatch.Vendor.TradeSpecialties - }, - PrimaryLocation = dispatch.WorkOrder?.Locations == null ? null : new - { - dispatch.WorkOrder.Locations.Id, - dispatch.WorkOrder.Locations.Name, - dispatch.WorkOrder.Locations.Address1, - dispatch.WorkOrder.Locations.City, - dispatch.WorkOrder.Locations.State, - dispatch.WorkOrder.Locations.Zip - }, - WorkOrders = linkedWOs, - Checklist = checklist, - Signoffs = signoffs, - Comments = comments, - UpliftRequests = upliftRequests, - AuditLog = auditLog - } + Data = dispatch }); } } diff --git a/Api.SeaHavenIndustries/Controllers/FollowUpController.cs b/Api.SeaHavenIndustries/Controllers/FollowUpController.cs index 02bdf0f..35099eb 100644 --- a/Api.SeaHavenIndustries/Controllers/FollowUpController.cs +++ b/Api.SeaHavenIndustries/Controllers/FollowUpController.cs @@ -3,12 +3,9 @@ using Api.SeaHavenIndustries.DTOs; using Data.SeaHavenIndustries; using FluentValidation; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using SeaHaven.Services.Interfaces; using SeaHaven.Services.DTOs; -using SeaHaven.DataServices.Interfaces; +using SeaHaven.Services.Interfaces; [ApiController] [Authorize] @@ -17,16 +14,10 @@ using SeaHaven.DataServices.Interfaces; public class FollowUpController : ControllerBase { private readonly IFollowUpService _followUpService; - private readonly IFollowUpDataService _followUpDataService; // Keep for direct entity operations - private readonly ApplicationDbContext _db; // Keep for User lookups - private readonly UserManager _users; - public FollowUpController(IFollowUpService followUpService, IFollowUpDataService followUpDataService, ApplicationDbContext db, UserManager users) + public FollowUpController(IFollowUpService followUpService) { _followUpService = followUpService; - _followUpDataService = followUpDataService; - _db = db; - _users = users; } [HttpGet("GetFollowUpList")] @@ -34,70 +25,31 @@ public class FollowUpController : ControllerBase string? search = "", string? status = "Open", int page = 1, - int pageSize = 12) + int pageSize = 12, + CancellationToken cancellationToken = default) { - status = string.IsNullOrWhiteSpace(status) ? "Open" : status; - - var pagedResult = await _followUpService.GetFollowUpsPagedAsync(page, pageSize, search, status); + var pagedResult = await _followUpService.GetFollowUpListAsync(page, pageSize, search, status, cancellationToken); var items = pagedResult.Items; - var rows = items.Select(x => new + var data = items.Select(x => new { - Id = x.Id, - Reason = x.Reason, - ScheduleDate = x.ScheduleDate, - ScheduleStartTime = x.ScheduleStartTime, - EmployeeId = x.EmployeeId, - WorkOrderId = x.WorkOrderId, - AccountId = x.AccountId, - LocationId = x.LocationId, - AccountName = x.AccountName ?? "", - AssignedTo = x.EmployeeName ?? "", - LocationName = x.LocationName ?? "", - WorkOrderNumber = "", - InternalWONumber = "", - Status = x.Status, - CreatedDate = x.CreatedDate, - CreatedById = (string?)null - }).ToList(); - - var creatorIds = rows - .Select(r => r.CreatedById) - .Where(id => !string.IsNullOrEmpty(id)) - .Distinct() - .ToList(); - - var creators = await _db.Users - .Where(u => creatorIds.Contains(u.Id)) - .Select(u => new { u.Id, u.FirstName, u.LastName, u.UserName }) - .ToDictionaryAsync(u => u.Id, u => u); - - string NameFor(string? userId) - { - if (string.IsNullOrEmpty(userId) || !creators.TryGetValue(userId, out var u)) return ""; - var full = ((u.FirstName ?? "") + " " + (u.LastName ?? "")).Trim(); - return string.IsNullOrWhiteSpace(full) ? (u.UserName ?? "") : full; - } - - var data = rows.Select(r => new - { - r.Id, - r.Reason, - r.ScheduleDate, - r.ScheduleStartTime, - r.EmployeeId, - r.WorkOrderId, - r.AccountId, - r.LocationId, - r.AccountName, - r.AssignedTo, - r.LocationName, - r.WorkOrderNumber, - r.InternalWONumber, - r.Status, - r.CreatedDate, - r.CreatedById, - CreatedByName = NameFor(r.CreatedById) + x.Id, + x.Reason, + x.ScheduleDate, + x.ScheduleStartTime, + x.EmployeeId, + x.WorkOrderId, + x.AccountId, + x.LocationId, + x.AccountName, + x.AssignedTo, + x.LocationName, + x.WorkOrderNumber, + x.InternalWONumber, + x.Status, + x.CreatedDate, + x.CreatedById, + x.CreatedByName }).ToList(); return Ok(new Pagination_DTO @@ -111,26 +63,12 @@ public class FollowUpController : ControllerBase } [HttpGet("GetById")] - public async Task GetById(int id) + public async Task GetById(int id, CancellationToken cancellationToken) { - var model = await _followUpDataService.GetByIdWithDetailsAsync(id); + var model = await _followUpService.GetFollowUpDetailAsync(id, cancellationToken); if (model == null) return NotFound(); - string createdByName = ""; - if (!string.IsNullOrEmpty(model.createdby)) - { - var u = await _db.Users - .Where(x => x.Id == model.createdby) - .Select(x => new { x.FirstName, x.LastName, x.UserName }) - .FirstOrDefaultAsync(); - if (u != null) - { - var n = ((u.FirstName ?? "") + " " + (u.LastName ?? "")).Trim(); - createdByName = string.IsNullOrWhiteSpace(n) ? (u.UserName ?? "") : n; - } - } - return Ok(new { model.Id, @@ -141,22 +79,20 @@ public class FollowUpController : ControllerBase model.WorkOrderId, model.AccountId, model.LocationId, - AccountName = model.Account?.Name ?? "", - AssignedTo = model.Employee != null - ? (model.Employee.FirstName + " " + model.Employee.LastName) - : "", - LocationName = model.Location?.Name ?? "", - WorkOrderNumber = model.WorkOrder?.WorkerOrderNumber ?? "", - InternalWONumber = model.WorkOrder?.InternalWONumber ?? "", + model.AccountName, + model.AssignedTo, + model.LocationName, + model.WorkOrderNumber, + model.InternalWONumber, model.Status, model.CreatedDate, - CreatedById = model.createdby, - CreatedByName = createdByName + model.CreatedById, + model.CreatedByName }); } [HttpPost("Create")] - public async Task Create(FollowUp_DTO dto) + public async Task Create(FollowUp_DTO dto, CancellationToken cancellationToken) { try { @@ -164,23 +100,9 @@ public class FollowUpController : ControllerBase if (userId == null) return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" }); - var model = new FollowUps - { - Reason = dto.Reason, - ScheduleDate = dto.ScheduleDate, - ScheduleStartTime = dto.ScheduleStartTime, - EmployeeId = dto.EmployeeId, - WorkOrderId = dto.WorkOrderId, - AccountId = dto.AccountId, - LocationId = dto.LocationId, - Status = "Open", - CreatedDate = DateTime.UtcNow, - createdby = userId - }; + var id = await _followUpService.CreateOpenFollowUpAsync(MapToCreateRequest(dto), userId, cancellationToken); - await _followUpDataService.AddAsync(model); - - return Ok(new DataResponse { Status = "200", Message = "Follow Up created", Data = model.Id }); + return Ok(new DataResponse { Status = "200", Message = "Follow Up created", Data = id }); } catch (ValidationException vex) { @@ -194,24 +116,11 @@ public class FollowUpController : ControllerBase } [HttpPost("Update")] - public async Task Update(FollowUp_DTO dto) + public async Task Update(FollowUp_DTO dto, CancellationToken cancellationToken) { try { - var model = await _followUpDataService.GetByIdAsync(dto.Id); - if (model == null) - return NotFound(new Response { Status = "Error", Message = "Follow Up not found" }); - - model.Reason = dto.Reason; - model.ScheduleDate = dto.ScheduleDate; - model.ScheduleStartTime = dto.ScheduleStartTime; - model.EmployeeId = dto.EmployeeId; - model.WorkOrderId = dto.WorkOrderId; - model.AccountId = dto.AccountId; - model.LocationId = dto.LocationId; - model.LastModificationTime = DateTime.UtcNow; - - await _followUpDataService.UpdateAsync(model); + await _followUpService.UpdateFollowUpFieldsAsync(dto.Id, MapToUpdateRequest(dto), cancellationToken); return Ok(new DataResponse { Status = "200", Message = "Follow Up updated" }); } catch (ValidationException vex) @@ -230,42 +139,32 @@ public class FollowUpController : ControllerBase } [HttpPost("Complete")] - public async Task Complete(int id) + public async Task Complete(int id, CancellationToken cancellationToken) { - var model = await _followUpDataService.GetByIdAsync(id); - if (model == null) return NotFound(); - - model.Status = "Completed"; - model.LastModificationTime = DateTime.UtcNow; - await _followUpDataService.UpdateAsync(model); + var found = await _followUpService.SetFollowUpStatusAsync(id, "Completed", cancellationToken); + if (!found) return NotFound(); return Ok(); } [HttpPost("Cancel")] - public async Task Cancel(int id) + public async Task Cancel(int id, CancellationToken cancellationToken) { - var model = await _followUpDataService.GetByIdAsync(id); - if (model == null) return NotFound(); - - model.Status = "Cancelled"; - model.LastModificationTime = DateTime.UtcNow; - await _followUpDataService.UpdateAsync(model); + var found = await _followUpService.SetFollowUpStatusAsync(id, "Cancelled", cancellationToken); + if (!found) return NotFound(); return Ok(); } [HttpPost("Delete")] - public async Task Delete(int id) + public async Task Delete(int id, CancellationToken cancellationToken) { try { - var model = await _followUpDataService.GetByIdAsync(id); - if (model == null) + var found = await _followUpService.RemoveFollowUpAsync(id, cancellationToken); + if (!found) return NotFound(new Response { Status = "Error", Message = "Follow Up not found" }); - await _followUpDataService.DeleteAsync(id); - return Ok(new DataResponse { Status = "200", Message = "Follow Up deleted" }); } catch (KeyNotFoundException) @@ -277,4 +176,33 @@ public class FollowUpController : ControllerBase return StatusCode(500, new Response { Status = "Error", Message = ex.Message }); } } + + private static FollowUpCreateRequestDTO MapToCreateRequest(FollowUp_DTO dto) + { + return new FollowUpCreateRequestDTO + { + Reason = dto.Reason, + ScheduleDate = dto.ScheduleDate, + ScheduleStartTime = dto.ScheduleStartTime, + EmployeeId = dto.EmployeeId, + WorkOrderId = dto.WorkOrderId, + AccountId = dto.AccountId, + LocationId = dto.LocationId + }; + } + + private static FollowUpUpdateRequestDTO MapToUpdateRequest(FollowUp_DTO dto) + { + return new FollowUpUpdateRequestDTO + { + Id = dto.Id, + Reason = dto.Reason, + ScheduleDate = dto.ScheduleDate, + ScheduleStartTime = dto.ScheduleStartTime, + EmployeeId = dto.EmployeeId, + WorkOrderId = dto.WorkOrderId, + AccountId = dto.AccountId, + LocationId = dto.LocationId + }; + } } diff --git a/Api.SeaHavenIndustries/Controllers/LocationController.cs b/Api.SeaHavenIndustries/Controllers/LocationController.cs index 4bfba79..b974264 100644 --- a/Api.SeaHavenIndustries/Controllers/LocationController.cs +++ b/Api.SeaHavenIndustries/Controllers/LocationController.cs @@ -3,11 +3,8 @@ using Data.SeaHavenIndustries; using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using SeaHaven.Services.Interfaces; using SeaHaven.Services.DTOs; -using SeaHaven.DataServices.Interfaces; -using System.Security.Claims; +using SeaHaven.Services.Interfaces; namespace Api.SeaHavenIndustries.Controllers { @@ -18,18 +15,16 @@ namespace Api.SeaHavenIndustries.Controllers public class LocationController : Controller { private readonly ILocationService _locationService; - private readonly ILocationDataService _locationDataService; // Keep for direct entity operations - public LocationController(ILocationService locationService, ILocationDataService locationDataService) + public LocationController(ILocationService locationService) { _locationService = locationService; - _locationDataService = locationDataService; } [HttpGet("GetLocationList")] - public async Task GetLocationList(string? search = "", int page = 1, int pageSize = 10) + public async Task GetLocationList(string? search = "", int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) { - var pagedResult = await _locationService.GetLocationsPagedAsync(page, pageSize, search); + var pagedResult = await _locationService.GetLocationListPagedAsync(page, pageSize, search, cancellationToken); var data = pagedResult.Items.Select(l => new { @@ -61,9 +56,9 @@ namespace Api.SeaHavenIndustries.Controllers } [HttpGet("{id}")] - public async Task GetLocationById(int id) + public async Task GetLocationById(int id, CancellationToken cancellationToken) { - var location = await _locationDataService.GetByIdAsync(id); + var location = await _locationService.GetLocationDetailAsync(id, cancellationToken); if (location == null) return NotFound(new Response { Status = "Error", Message = "Location not found" }); @@ -89,25 +84,11 @@ namespace Api.SeaHavenIndustries.Controllers } [HttpPost] - public async Task AddLocation([FromBody] Location_DTO model) + public async Task AddLocation([FromBody] Location_DTO model, CancellationToken cancellationToken) { try { - var location = new Locations - { - Name = model.Name, - Title = model.Title, - Address1 = model.Address, - City = model.City, - State = model.State, - Zip = model.ZipCode, - PhoneNumber = model.Phone, - Email = model.ContactEmail, - Status = model.Status, - CreatedDate = DateTime.Now - }; - - await _locationDataService.AddAsync(location); + await _locationService.CreateLocationFromRequestAsync(MapToCreateRequest(model), cancellationToken); return Ok(new DataResponse { Message = "Location Created Successfully", Status = "200" }); } catch (ValidationException vex) @@ -122,26 +103,11 @@ namespace Api.SeaHavenIndustries.Controllers } [HttpPut("{id}")] - public async Task EditLocation(int id, [FromBody] EditLocation_DTO model) + public async Task EditLocation(int id, [FromBody] EditLocation_DTO model, CancellationToken cancellationToken) { try { - var exist = await _locationDataService.GetByIdAsync(id); - if (exist == null) - return NotFound(new Response { Status = "Error", Message = "Location not found" }); - - exist.Name = model.Name; - exist.Title = model.Title; - exist.Address1 = model.Address; - exist.City = model.City; - exist.State = model.State; - exist.Zip = model.ZipCode; - exist.PhoneNumber = model.Phone; - exist.Email = model.ContactEmail; - exist.Status = model.Status; - exist.LastModificationTime = DateTime.Now; - - await _locationDataService.UpdateAsync(exist); + await _locationService.UpdateLocationFromRequestAsync(id, MapToUpdateRequest(model), cancellationToken); return Ok(new DataResponse { Message = "Location Updated Successfully", Status = "200" }); } catch (ValidationException vex) @@ -160,15 +126,14 @@ namespace Api.SeaHavenIndustries.Controllers } [HttpDelete("{id}")] - public async Task DeleteLocation(int id) + public async Task DeleteLocation(int id, CancellationToken cancellationToken) { try { - var exist = await _locationDataService.GetByIdAsync(id); - if (exist == null) + var found = await _locationService.DeleteLocationByIdAsync(id, cancellationToken); + if (!found) return NotFound(new Response { Status = "Error", Message = "Location not found" }); - await _locationDataService.DeleteAsync(id); return Ok(new DataResponse { Message = "Location Deleted Successfully", Status = "200" }); } catch (KeyNotFoundException) @@ -182,9 +147,41 @@ namespace Api.SeaHavenIndustries.Controllers } [HttpGet("DeleteLocation")] - public async Task DeleteLocationByQuery(int id) + public async Task DeleteLocationByQuery(int id, CancellationToken cancellationToken) { - return await DeleteLocation(id); + return await DeleteLocation(id, cancellationToken); + } + + private static LocationCreateRequestDTO MapToCreateRequest(Location_DTO model) + { + return new LocationCreateRequestDTO + { + Name = model.Name, + Title = model.Title, + Address = model.Address, + City = model.City, + State = model.State, + ZipCode = model.ZipCode, + Phone = model.Phone, + ContactEmail = model.ContactEmail, + Status = model.Status + }; + } + + private static LocationUpdateRequestDTO MapToUpdateRequest(EditLocation_DTO model) + { + return new LocationUpdateRequestDTO + { + Name = model.Name, + Title = model.Title, + Address = model.Address, + City = model.City, + State = model.State, + ZipCode = model.ZipCode, + Phone = model.Phone, + ContactEmail = model.ContactEmail, + Status = model.Status + }; } } } diff --git a/Api.SeaHavenIndustries/Controllers/SettingsController.cs b/Api.SeaHavenIndustries/Controllers/SettingsController.cs index 5a0510a..38c31b4 100644 --- a/Api.SeaHavenIndustries/Controllers/SettingsController.cs +++ b/Api.SeaHavenIndustries/Controllers/SettingsController.cs @@ -2,10 +2,8 @@ using Api.SeaHavenIndustries.Helper.ViewModel; using Data.SeaHavenIndustries; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using static System.Runtime.InteropServices.JavaScript.JSType; +using SeaHaven.Services.Interfaces; namespace Api.SeaHavenIndustries.Controllers { @@ -14,24 +12,20 @@ namespace Api.SeaHavenIndustries.Controllers [Route("[controller]")] public class SettingsController : Controller { - private readonly UserManager _userManager; - private readonly ApplicationDbContext _db; - public SettingsController(UserManager userManager, ApplicationDbContext db) + private readonly ISettingsService _settingsService; + + public SettingsController(ISettingsService settingsService) { - _userManager = userManager; - _db = db; + _settingsService = settingsService; } [HttpPost] [Route("AddCategory")] - public async Task AddCategory(Category_DTO model) + public async Task AddCategory(Category_DTO model, CancellationToken cancellationToken) { try { - Category category = new Category(); - category.Name = model.Name; - _db.Categories.Add(category); - await _db.SaveChangesAsync(); + await _settingsService.AddCategoryAsync(model.Name, cancellationToken); return Ok(new DataResponse { Message = "Successfully Saved Data", Status = "200" }); } catch (Exception ex) @@ -42,16 +36,13 @@ namespace Api.SeaHavenIndustries.Controllers [HttpPost] [Route("EditCategory")] - public async Task EditCategory(EditCategory_DTO model) + public async Task EditCategory(EditCategory_DTO model, CancellationToken cancellationToken) { try { - var exist = _db.Categories.Where(w => w.Id == model.Id).FirstOrDefault(); - if (exist == null) + var found = await _settingsService.UpdateCategoryAsync(model.Id, model.Name, cancellationToken); + if (!found) return BadRequest(new Response { Status = "Error", Message = "Category not found" }); - exist.Name = model.Name; - _db.Categories.Update(exist); - await _db.SaveChangesAsync(); return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" }); } catch (Exception ex) @@ -62,30 +53,13 @@ namespace Api.SeaHavenIndustries.Controllers [HttpGet] [Route("DeleteCategory")] - public async Task DeleteCategory(int id) + public async Task DeleteCategory(int id, CancellationToken cancellationToken) { try { - var exist = _db.Categories.Where(w => w.Id == id).FirstOrDefault(); - if (exist != null) - { - var chkexist = _db.Templates.Where(w => w.CategoryId == id).ToList(); - if (chkexist != null) - { - _db.RemoveRange(chkexist); - } - var chkworkordercategory = _db.workOrderCategories.Where(w => w.CategoryId == id).ToList(); - if (chkworkordercategory != null) - { - _db.RemoveRange(chkworkordercategory); - } - _db.Remove(exist); - } - else - { + var found = await _settingsService.DeleteCategoryAsync(id, cancellationToken); + if (!found) return BadRequest(new Response { Status = "Error", Message = "ID not matched!" }); - } - await _db.SaveChangesAsync(); return Ok(new DataResponse { Message = "Deleted Successfully", Status = "200" }); } catch (Exception ex) @@ -93,28 +67,21 @@ namespace Api.SeaHavenIndustries.Controllers return BadRequest(new Response { Status = "Error", Message = ex.Message }); } } - [HttpGet] [Route("GetCategoryList")] - public IActionResult GetCategoryList(int page = 1, int pageSize = 10) + public async Task GetCategoryList(int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) { - var totalCount = _db.Categories.Count(); - var data = _db.Categories.Select(s => new - { - s.Id, - s.Name - }).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize) - .Take(pageSize).ToList(); + var (items, total) = await _settingsService.GetCategoryListAsync(page, pageSize, cancellationToken); - var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); + var totalPages = (int)Math.Ceiling(total / (double)pageSize); var viewModel = new Pagination_DTO { - Data = data, + Data = items, PageNumber = page, PageSize = pageSize, - TotalCount = totalCount, + TotalCount = total, TotalPages = totalPages }; return Ok(viewModel); @@ -122,24 +89,20 @@ namespace Api.SeaHavenIndustries.Controllers [HttpGet] [Route("GetTemplates")] - public IActionResult GetTemplates(int page = 1, int pageSize = 10) + public async Task GetTemplates(int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) { try { - var totalCount = _db.workOrders.Where(w=> w.istemplate == true).Count(); - var data = _db.workOrders.Where(w => w.istemplate == true).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize) - .Take(pageSize).ToList(); + var (items, total) = await _settingsService.GetTemplatesAsync(page, pageSize, cancellationToken); - // Calculate the number of pages - var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); + var totalPages = (int)Math.Ceiling(total / (double)pageSize); - // Create a view model containing the paged items and pagination metadata var viewModel = new Pagination_DTO { - Data = data, + Data = items, PageNumber = page, PageSize = pageSize, - TotalCount = totalCount, + TotalCount = total, TotalPages = totalPages }; return Ok(viewModel); @@ -152,42 +115,30 @@ namespace Api.SeaHavenIndustries.Controllers [HttpGet] [Route("GetUsers")] - public async Task GetUsers(string? search = "", int page = 1, int pageSize = 10) + public async Task GetUsers(string? search = "", int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) { try { - var totalCount = _db.Users.Count(); - var allUsers = _db.Users.Where(w => (w.FirstName ?? "").ToLower().Contains((search ?? "").ToLower())).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize) - .Take(pageSize).ToList(); + var (users, total) = await _settingsService.GetUsersAsync(search, page, pageSize, cancellationToken); - // Calculate the number of pages - var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); - - var filteredUsers = new List(); - - foreach (var user in allUsers) + var filteredUsers = users.Select(u => new UserRoleViewModel { - var roles = await _userManager.GetRolesAsync(user); - if (!roles.Contains("Admin")) - { - filteredUsers.Add(new UserRoleViewModel - { - RoleName = roles.Count() > 0 ? roles.First() : "", - Email = user.Email, - UserName = user.FirstName, - Date = user.CreatedDate?.Date.ToString(), - Status = user.UniqueName, - Id = user.Id - }); - } - } - // Create a view model containing the paged items and pagination metadata + RoleName = u.RoleName, + Email = u.Email, + UserName = u.UserName, + Date = u.Date, + Status = u.Status, + Id = u.Id + }).ToList(); + + var totalPages = (int)Math.Ceiling(total / (double)pageSize); + var viewModel = new Pagination_DTO { Data = filteredUsers, PageNumber = page, PageSize = pageSize, - TotalCount = totalCount, + TotalCount = total, TotalPages = totalPages }; return Ok(viewModel); @@ -200,35 +151,13 @@ namespace Api.SeaHavenIndustries.Controllers [HttpGet] [Route("GetRoles")] - public async Task GetRoles() + public async Task GetRoles(CancellationToken cancellationToken) { try { - var data = _db.Roles.ToList(); - List roles = new List(); - foreach (var role in data) - { - if (role.Name == "Admin") - { - roles.Add(new Roles_DTO { Name = role.Name, Description = "Full Access" }); - } - else if(role.Name == "Supervisor") - { - roles.Add(new Roles_DTO { Name = role.Name, Description = "Access to Team's Work Orders Only" }); - - } - else if(role.Name == "Manager") - { - roles.Add(new Roles_DTO { Name = role.Name, Description = "Full access accept quotes" }); - - } - else if(role.Name =="User") - { - roles.Add(new Roles_DTO { Name = role.Name, Description = "Access to Assigned Work Orders Only" }); - - } - } - return Ok(roles); + var roles = await _settingsService.GetRolesAsync(cancellationToken); + var result = roles.Select(r => new Roles_DTO { Name = r.Name, Description = r.Description }).ToList(); + return Ok(result); } catch (Exception ex) { diff --git a/SeaHaven.DataServices/Dto/DispatchReadModels.cs b/SeaHaven.DataServices/Dto/DispatchReadModels.cs new file mode 100644 index 0000000..45dc210 --- /dev/null +++ b/SeaHaven.DataServices/Dto/DispatchReadModels.cs @@ -0,0 +1,169 @@ +namespace SeaHaven.DataServices.Dto +{ + public class DispatchListItemDTO + { + public int Id { get; set; } + public string? DispatchNumber { get; set; } + public string? PONumber { get; set; } + public decimal? NTEAmount { get; set; } + public string? Status { get; set; } + public DateTime? DispatchedAt { get; set; } + public DateTime? ScheduledDate { get; set; } + public DateTime? CompletedDate { get; set; } + public DateTime? AcknowledgedAt { get; set; } + public DateTime? VerifiedAt { get; set; } + public int? VendorId { get; set; } + public string? VendorName { get; set; } + public int? WorkOrderId { get; set; } + public string? PrimaryWOInternalNumber { get; set; } + public string? PrimaryWOTitle { get; set; } + public string? LocationName { get; set; } + public int WorkOrderCount { get; set; } + public int PendingUpliftCount { get; set; } + } + + public class DispatchListResult + { + public IReadOnlyList Items { get; set; } = Array.Empty(); + public int Total { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + } + + public class DispatchListQuery + { + public string? Search { get; set; } + public string? Status { get; set; } + public int? VendorId { get; set; } + public DateTime? DateFrom { get; set; } + public DateTime? DateTo { get; set; } + public string? SortBy { get; set; } + public string? SortDir { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + } + + public class DispatchDetailVendorDTO + { + public int Id { get; set; } + public string? CompanyName { get; set; } + public string? ContactName { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public string? Address { get; set; } + public string? City { get; set; } + public string? State { get; set; } + public string? Zip { get; set; } + public string? TradeSpecialties { get; set; } + } + + public class DispatchPrimaryLocationDTO + { + public int Id { get; set; } + public string? Name { get; set; } + public string? Address1 { get; set; } + public string? City { get; set; } + public string? State { get; set; } + public string? Zip { get; set; } + } + + public class DispatchLinkedWorkOrderDTO + { + public int WorkOrderId { get; set; } + public string? InternalWONumber { get; set; } + public string? Title { get; set; } + public string? Priority { get; set; } + public string? Status { get; set; } + public string? LocationName { get; set; } + public string? Trade { get; set; } + public string? SubTrade { get; set; } + public string? Problem { get; set; } + } + + public class DispatchChecklistDTO + { + 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 DispatchSignoffDTO + { + 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 DispatchCommentDTO + { + public int Id { get; set; } + public string? Commenttext { get; set; } + public string? Commenter { get; set; } + public string? CommentType { get; set; } + public string? RecordType { get; set; } + public DateTime? CreatedDate { get; set; } + public string? UserId { get; set; } + public string UserName { get; set; } = ""; + } + + public class DispatchUpliftRequestDTO + { + public int Id { get; set; } + public decimal? CurrentNTE { get; set; } + public decimal RequestedNTE { get; set; } + public decimal Delta { get; set; } + public string? VendorReason { get; set; } + public string? Status { get; set; } + public int RequiredTier { get; set; } + public string? RequestedByVendorName { get; set; } + public DateTime? RequestedAt { get; set; } + public DateTime? DecidedAt { get; set; } + public string? DecisionNote { get; set; } + public string DecidedByName { get; set; } = ""; + } + + public class DispatchAuditLogDTO + { + public int Id { get; set; } + public string? Action { get; set; } + public string? FieldName { get; set; } + public string? OldValue { get; set; } + public string? NewValue { get; set; } + public DateTime? CreatedAt { get; set; } + public string? UserId { get; set; } + public string UserName { get; set; } = ""; + } + + public class DispatchDetailDTO + { + 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 DateTime? AcknowledgedAt { get; set; } + public DateTime? VerifiedAt { get; set; } + public string? VerifiedBy { get; set; } + public bool EmailSent { get; set; } + public int? WorkOrderId { get; set; } + public DispatchDetailVendorDTO? Vendor { get; set; } + public DispatchPrimaryLocationDTO? PrimaryLocation { get; set; } + public IReadOnlyList WorkOrders { get; set; } = Array.Empty(); + public IReadOnlyList Checklist { get; set; } = Array.Empty(); + public IReadOnlyList Signoffs { get; set; } = Array.Empty(); + public IReadOnlyList Comments { get; set; } = Array.Empty(); + public IReadOnlyList UpliftRequests { get; set; } = Array.Empty(); + public IReadOnlyList AuditLog { get; set; } = Array.Empty(); + } +} diff --git a/SeaHaven.DataServices/Dto/SettingsReadModels.cs b/SeaHaven.DataServices/Dto/SettingsReadModels.cs new file mode 100644 index 0000000..99e4057 --- /dev/null +++ b/SeaHaven.DataServices/Dto/SettingsReadModels.cs @@ -0,0 +1,8 @@ +namespace SeaHaven.DataServices.Dto +{ + public class SettingsCategoryItemDTO + { + public int Id { get; set; } + public string? Name { get; set; } + } +} diff --git a/SeaHaven.DataServices/Implementation/DispatchDataService.cs b/SeaHaven.DataServices/Implementation/DispatchDataService.cs index 4a057f6..f4d0c31 100644 --- a/SeaHaven.DataServices/Implementation/DispatchDataService.cs +++ b/SeaHaven.DataServices/Implementation/DispatchDataService.cs @@ -1,5 +1,6 @@ using Data.SeaHavenIndustries; using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Dto; using SeaHaven.DataServices.Interfaces; namespace SeaHaven.DataServices.Implementation @@ -216,5 +217,346 @@ namespace SeaHaven.DataServices.Implementation await _context.DispatchWorkOrders.AddAsync(dispatchWorkOrder); await _context.SaveChangesAsync(); } + + public async Task GetListAsync(DispatchListQuery query, CancellationToken cancellationToken) + { + var q = _context.Dispatches + .Include(d => d.Vendor) + .Include(d => d.WorkOrder).ThenInclude(w => w!.Locations) + .AsNoTracking() + .Where(d => d.IsDeleted == null || d.IsDeleted == false); + + if (!string.IsNullOrWhiteSpace(query.Search)) + { + var s = query.Search.ToLower(); + q = q.Where(d => + (d.DispatchNumber ?? "").ToLower().Contains(s) || + (d.PONumber ?? "").ToLower().Contains(s) || + (d.Vendor != null && (d.Vendor.CompanyName ?? "").ToLower().Contains(s)) || + (d.WorkOrder != null && (d.WorkOrder.InternalWONumber ?? "").ToLower().Contains(s)) || + (d.WorkOrder != null && (d.WorkOrder.WorkerOrderTitle ?? "").ToLower().Contains(s)) || + (d.WorkOrder != null && d.WorkOrder.Locations != null && (d.WorkOrder.Locations.Name ?? "").ToLower().Contains(s)) + ); + } + + if (!string.IsNullOrWhiteSpace(query.Status)) + q = q.Where(d => d.Status == query.Status); + + if (query.VendorId.HasValue) + q = q.Where(d => d.VendorId == query.VendorId.Value); + + if (query.DateFrom.HasValue) + q = q.Where(d => d.DispatchedAt >= query.DateFrom.Value); + + if (query.DateTo.HasValue) + { + var end = query.DateTo.Value.Date.AddDays(1); + q = q.Where(d => d.DispatchedAt < end); + } + + var total = await q.CountAsync(cancellationToken); + + var dir = (query.SortDir ?? "desc").ToLowerInvariant() == "asc"; + switch ((query.SortBy ?? "").ToLowerInvariant()) + { + case "ponumber": + q = dir ? q.OrderBy(d => d.PONumber) : q.OrderByDescending(d => d.PONumber); + break; + case "dispatchnumber": + q = dir ? q.OrderBy(d => d.DispatchNumber) : q.OrderByDescending(d => d.DispatchNumber); + break; + case "vendor": + q = dir ? q.OrderBy(d => d.Vendor!.CompanyName) : q.OrderByDescending(d => d.Vendor!.CompanyName); + break; + case "nte": + case "nteamount": + q = dir ? q.OrderBy(d => d.NTEAmount) : q.OrderByDescending(d => d.NTEAmount); + break; + case "status": + q = dir ? q.OrderBy(d => d.Status) : q.OrderByDescending(d => d.Status); + break; + case "scheduleddate": + q = dir ? q.OrderBy(d => d.ScheduledDate) : q.OrderByDescending(d => d.ScheduledDate); + break; + case "completeddate": + q = dir ? q.OrderBy(d => d.CompletedDate) : q.OrderByDescending(d => d.CompletedDate); + break; + default: + q = dir ? q.OrderBy(d => d.DispatchedAt) : q.OrderByDescending(d => d.DispatchedAt); + break; + } + + var rows = await q + .Skip((Math.Max(query.Page, 1) - 1) * query.PageSize) + .Take(query.PageSize) + .Select(d => new DispatchListItemDTO + { + Id = d.Id, + DispatchNumber = d.DispatchNumber, + PONumber = d.PONumber, + NTEAmount = d.NTEAmount, + Status = d.Status, + DispatchedAt = d.DispatchedAt, + ScheduledDate = d.ScheduledDate, + CompletedDate = d.CompletedDate, + AcknowledgedAt = d.AcknowledgedAt, + VerifiedAt = d.VerifiedAt, + VendorId = d.VendorId, + VendorName = d.Vendor != null ? d.Vendor.CompanyName : null, + WorkOrderId = d.WorkOrderId, + PrimaryWOInternalNumber = d.WorkOrder != null ? d.WorkOrder.InternalWONumber : null, + PrimaryWOTitle = d.WorkOrder != null ? d.WorkOrder.WorkerOrderTitle : null, + LocationName = d.WorkOrder != null && d.WorkOrder.Locations != null ? d.WorkOrder.Locations.Name : null, + WorkOrderCount = _context.DispatchWorkOrders.Count(dwo => dwo.DispatchId == d.Id), + PendingUpliftCount = _context.DispatchUpliftRequests.Count(u => u.DispatchId == d.Id && u.Status == "Pending" && (u.IsDeleted == null || u.IsDeleted == false)) + }) + .ToListAsync(cancellationToken); + + return new DispatchListResult + { + Items = rows, + Total = total, + Page = query.Page, + PageSize = query.PageSize + }; + } + + public async Task GetDetailAsync(int id, CancellationToken cancellationToken) + { + var dispatch = await _context.Dispatches + .Include(d => d.Vendor) + .Include(d => d.WorkOrder).ThenInclude(w => w!.Locations) + .AsNoTracking() + .FirstOrDefaultAsync(d => d.Id == id, cancellationToken); + if (dispatch == null) return null; + + var linked = await _context.DispatchWorkOrders + .AsNoTracking() + .Where(dwo => dwo.DispatchId == id) + .Select(dwo => new DispatchLinkedWorkOrderDTO + { + WorkOrderId = dwo.WorkOrderId, + InternalWONumber = dwo.WorkOrder != null ? dwo.WorkOrder.InternalWONumber : null, + Title = dwo.WorkOrder != null ? dwo.WorkOrder.WorkerOrderTitle : null, + Priority = dwo.WorkOrder != null ? dwo.WorkOrder.Priority : null, + Status = dwo.WorkOrder != null ? dwo.WorkOrder.Status : null, + LocationName = dwo.WorkOrder != null && dwo.WorkOrder.Locations != null ? dwo.WorkOrder.Locations.Name : null, + Trade = dwo.WorkOrder != null ? dwo.WorkOrder.Trade : null, + SubTrade = dwo.WorkOrder != null ? dwo.WorkOrder.SubTrade : null, + Problem = dwo.WorkOrder != null ? dwo.WorkOrder.Problem : null + }) + .ToListAsync(cancellationToken); + + var linkedWOs = linked; + if (linkedWOs.Count == 0 && dispatch.WorkOrderId.HasValue) + { + var wo = await _context.workOrders + .Include(w => w.Locations) + .AsNoTracking() + .FirstOrDefaultAsync(w => w.Id == dispatch.WorkOrderId.Value, cancellationToken); + if (wo != null) + { + linkedWOs = new List + { + new DispatchLinkedWorkOrderDTO + { + WorkOrderId = wo.Id, + InternalWONumber = wo.InternalWONumber, + Title = wo.WorkerOrderTitle, + Priority = wo.Priority, + Status = wo.Status, + LocationName = wo.Locations != null ? wo.Locations.Name : null, + Trade = wo.Trade, + SubTrade = wo.SubTrade, + Problem = wo.Problem + } + }; + } + } + + var checklist = await _context.DispatchChecklistItems + .AsNoTracking() + .Where(c => c.DispatchId == id && (c.IsDeleted == null || c.IsDeleted == false)) + .OrderBy(c => c.SortOrder) + .Select(c => new DispatchChecklistDTO + { + Id = c.Id, + ItemText = c.ItemText, + IsCompleted = c.IsCompleted, + CompletedBy = c.CompletedBy, + CompletedAt = c.CompletedAt, + SortOrder = c.SortOrder, + WorkOrderId = c.WorkOrderId + }) + .ToListAsync(cancellationToken); + + var signoffs = await _context.DispatchSignoffs + .AsNoTracking() + .Where(s => s.DispatchId == id && (s.IsDeleted == null || s.IsDeleted == false)) + .Select(s => new DispatchSignoffDTO + { + Id = s.Id, + SignoffType = s.SignoffType, + Name = s.Name, + Signature = s.Signature, + SignatureMethod = s.SignatureMethod, + SignedAt = s.SignedAt + }) + .ToListAsync(cancellationToken); + + var rawComments = await (from c in _context.Comments + where c.DispatchId == id && (c.IsDeleted == null || c.IsDeleted == false) + join u in _context.Users on c.UserId equals u.Id into users + from u in users.DefaultIfEmpty() + orderby c.CreatedDate + select new + { + c.Id, + c.Commenttext, + c.Commenter, + c.CommentType, + c.RecordType, + c.CreatedDate, + c.UserId, + UserFirstName = u != null ? u.FirstName : null, + UserLastName = u != null ? u.LastName : null + }) + .ToListAsync(cancellationToken); + + var comments = rawComments.Select(c => new DispatchCommentDTO + { + Id = c.Id, + Commenttext = c.Commenttext, + Commenter = c.Commenter, + CommentType = c.CommentType, + RecordType = c.RecordType, + CreatedDate = c.CreatedDate, + UserId = c.UserId, + UserName = string.Join(" ", new[] { c.UserFirstName, c.UserLastName } + .Where(s => !string.IsNullOrWhiteSpace(s))).Trim() + }).ToList(); + + var rawUplifts = await (from u in _context.DispatchUpliftRequests + where u.DispatchId == id && (u.IsDeleted == null || u.IsDeleted == false) + join dec in _context.Users on u.DecidedByUserId equals dec.Id into decs + from dec in decs.DefaultIfEmpty() + orderby u.CreatedDate descending + select new + { + u.Id, + u.CurrentNTE, + u.RequestedNTE, + u.VendorReason, + u.Status, + u.RequiredTier, + u.RequestedByVendorName, + RequestedAt = u.CreatedDate, + u.DecidedAt, + u.DecisionNote, + DecidedByFirstName = dec != null ? dec.FirstName : null, + DecidedByLastName = dec != null ? dec.LastName : null + }) + .ToListAsync(cancellationToken); + + var upliftRequests = rawUplifts.Select(u => new DispatchUpliftRequestDTO + { + Id = u.Id, + CurrentNTE = u.CurrentNTE, + RequestedNTE = u.RequestedNTE, + Delta = u.RequestedNTE - (u.CurrentNTE ?? 0m), + VendorReason = u.VendorReason, + Status = u.Status, + RequiredTier = u.RequiredTier, + RequestedByVendorName = u.RequestedByVendorName, + RequestedAt = u.RequestedAt, + DecidedAt = u.DecidedAt, + DecisionNote = u.DecisionNote, + DecidedByName = string.Join(" ", new[] { u.DecidedByFirstName, u.DecidedByLastName } + .Where(s => !string.IsNullOrWhiteSpace(s))).Trim() + }).ToList(); + + var dispatchNumber = dispatch.DispatchNumber ?? ""; + var workOrderIds = linkedWOs.Select(l => l.WorkOrderId).Where(wid => wid > 0).ToList(); + + var rawAudit = await (from a in _context.WorkOrderAuditLogs + where workOrderIds.Contains(a.WorkOrderId) + && a.FieldName != null + && a.FieldName.Contains(dispatchNumber) + join u in _context.Users on a.UserId equals u.Id into users + from u in users.DefaultIfEmpty() + orderby a.CreatedAt descending + select new + { + a.Id, + a.Action, + a.FieldName, + a.OldValue, + a.NewValue, + a.CreatedAt, + a.UserId, + UserFirstName = u != null ? u.FirstName : null, + UserLastName = u != null ? u.LastName : null + }) + .ToListAsync(cancellationToken); + + var auditLog = rawAudit.Select(a => new DispatchAuditLogDTO + { + Id = a.Id, + Action = a.Action, + FieldName = a.FieldName, + OldValue = a.OldValue, + NewValue = a.NewValue, + CreatedAt = a.CreatedAt, + UserId = a.UserId, + UserName = string.Join(" ", new[] { a.UserFirstName, a.UserLastName } + .Where(s => !string.IsNullOrWhiteSpace(s))).Trim() + }).ToList(); + + return new DispatchDetailDTO + { + 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, + AcknowledgedAt = dispatch.AcknowledgedAt, + VerifiedAt = dispatch.VerifiedAt, + VerifiedBy = dispatch.VerifiedBy, + EmailSent = dispatch.EmailSent, + WorkOrderId = dispatch.WorkOrderId, + Vendor = dispatch.Vendor == null ? null : new DispatchDetailVendorDTO + { + Id = dispatch.Vendor.Id, + CompanyName = dispatch.Vendor.CompanyName, + ContactName = dispatch.Vendor.ContactName, + Email = dispatch.Vendor.Email, + Phone = dispatch.Vendor.Phone, + Address = dispatch.Vendor.Address, + City = dispatch.Vendor.City, + State = dispatch.Vendor.State, + Zip = dispatch.Vendor.Zip, + TradeSpecialties = dispatch.Vendor.TradeSpecialties + }, + PrimaryLocation = dispatch.WorkOrder?.Locations == null ? null : new DispatchPrimaryLocationDTO + { + Id = dispatch.WorkOrder.Locations.Id, + Name = dispatch.WorkOrder.Locations.Name, + Address1 = dispatch.WorkOrder.Locations.Address1, + City = dispatch.WorkOrder.Locations.City, + State = dispatch.WorkOrder.Locations.State, + Zip = dispatch.WorkOrder.Locations.Zip + }, + WorkOrders = linkedWOs, + Checklist = checklist, + Signoffs = signoffs, + Comments = comments, + UpliftRequests = upliftRequests, + AuditLog = auditLog + }; + } } } diff --git a/SeaHaven.DataServices/Implementation/FollowUpDataService.cs b/SeaHaven.DataServices/Implementation/FollowUpDataService.cs index 4f34528..4eebe82 100644 --- a/SeaHaven.DataServices/Implementation/FollowUpDataService.cs +++ b/SeaHaven.DataServices/Implementation/FollowUpDataService.cs @@ -120,5 +120,138 @@ namespace SeaHaven.DataServices.Implementation { return await _context.FollowUps.CountAsync(); } + + public async Task> GetListPagedAsync( + int page, + int pageSize, + string? search, + string? status, + CancellationToken cancellationToken) + { + search ??= ""; + status = string.IsNullOrWhiteSpace(status) ? "Open" : status; + + var query = _context.FollowUps + .Include(x => x.Employee) + .Include(x => x.Account) + .Include(x => x.Location) + .AsNoTracking() + .Where(x => x.Status == status); + + if (!string.IsNullOrWhiteSpace(search)) + { + var s = search.ToLower(); + query = query.Where(x => + (x.Reason ?? "").ToLower().Contains(s) || + (x.Account != null && (x.Account.Name ?? "").ToLower().Contains(s)) || + (x.Employee != null && + (((x.Employee.FirstName ?? "") + " " + (x.Employee.LastName ?? "")) + .ToLower().Contains(s)) + ) + ); + } + + return await query + .OrderByDescending(f => f.Id) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + } + + public async Task GetListTotalAsync(string? search, string? status, CancellationToken cancellationToken) + { + search ??= ""; + status = string.IsNullOrWhiteSpace(status) ? "Open" : status; + + var query = _context.FollowUps + .AsNoTracking() + .Where(x => x.Status == status); + + if (!string.IsNullOrWhiteSpace(search)) + { + var s = search.ToLower(); + query = query.Where(x => + (x.Reason ?? "").ToLower().Contains(s) || + (x.Account != null && (x.Account.Name ?? "").ToLower().Contains(s)) || + (x.Employee != null && + (((x.Employee.FirstName ?? "") + " " + (x.Employee.LastName ?? "")) + .ToLower().Contains(s)) + ) + ); + } + + return await query.CountAsync(cancellationToken); + } + + public async Task GetDetailByIdAsync(int id, CancellationToken cancellationToken) + { + return await _context.FollowUps + .Include(x => x.Employee) + .Include(x => x.Account) + .Include(x => x.Location) + .AsNoTracking() + .FirstOrDefaultAsync(f => f.Id == id, cancellationToken); + } + + public async Task GetByIdForUpdateAsync(int id, CancellationToken cancellationToken) + { + return await _context.FollowUps.FindAsync(new object[] { id }, cancellationToken); + } + + public async Task GetUserNameAsync(string? userId, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(userId)) + return ""; + + var u = await _context.Users + .AsNoTracking() + .Where(x => x.Id == userId) + .Select(x => new { x.FirstName, x.LastName, x.UserName }) + .FirstOrDefaultAsync(cancellationToken); + + if (u == null) + return ""; + + var n = ((u.FirstName ?? "") + " " + (u.LastName ?? "")).Trim(); + return string.IsNullOrWhiteSpace(n) ? (u.UserName ?? "") : n; + } + + public async Task AddAsync(FollowUps followUp, CancellationToken cancellationToken) + { + followUp.CreatedDate = DateTime.Now; + await _context.FollowUps.AddAsync(followUp, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return followUp; + } + + public async Task UpdateAsync(FollowUps followUp, CancellationToken cancellationToken) + { + followUp.LastModificationTime = DateTime.Now; + _context.FollowUps.Update(followUp); + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task SetStatusAsync(int id, string status, CancellationToken cancellationToken) + { + var entity = await _context.FollowUps.FindAsync(new object[] { id }, cancellationToken); + if (entity == null) + return false; + + entity.Status = status; + entity.LastModificationTime = DateTime.Now; + await _context.SaveChangesAsync(cancellationToken); + return true; + } + + public async Task RemoveByIdAsync(int id, CancellationToken cancellationToken) + { + var entity = await _context.FollowUps.FindAsync(new object[] { id }, cancellationToken); + if (entity == null) + return false; + + _context.FollowUps.Remove(entity); + await _context.SaveChangesAsync(cancellationToken); + return true; + } } } diff --git a/SeaHaven.DataServices/Implementation/LocationDataService.cs b/SeaHaven.DataServices/Implementation/LocationDataService.cs index 65c46df..397dc1d 100644 --- a/SeaHaven.DataServices/Implementation/LocationDataService.cs +++ b/SeaHaven.DataServices/Implementation/LocationDataService.cs @@ -95,6 +95,69 @@ namespace SeaHaven.DataServices.Implementation return await _context.Locations.CountAsync(); } + public async Task<(List Items, int TotalCount)> GetListPagedAsync( + int page, + int pageSize, + string? search, + CancellationToken cancellationToken) + { + var query = _context.Locations.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(search)) + { + query = query.Where(l => + l.Name!.Contains(search) || + l.City!.Contains(search)); + } + + var totalCount = await query.CountAsync(cancellationToken); + var items = await query + .OrderBy(l => l.Name) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return (items, totalCount); + } + + public async Task GetDetailByIdAsync(int id, CancellationToken cancellationToken) + { + return await _context.Locations + .AsNoTracking() + .FirstOrDefaultAsync(l => l.Id == id, cancellationToken); + } + + public async Task GetByIdForUpdateAsync(int id, CancellationToken cancellationToken) + { + return await _context.Locations.FindAsync(new object[] { id }, cancellationToken); + } + + public async Task AddAsync(Locations location, CancellationToken cancellationToken) + { + location.CreatedDate = DateTime.Now; + await _context.Locations.AddAsync(location, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return location; + } + + public async Task UpdateAsync(Locations location, CancellationToken cancellationToken) + { + location.LastModificationTime = DateTime.Now; + _context.Locations.Update(location); + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task DeleteByIdAsync(int id, CancellationToken cancellationToken) + { + var entity = await _context.Locations.FindAsync(new object[] { id }, cancellationToken); + if (entity == null) + return false; + + _context.Locations.Remove(entity); + await _context.SaveChangesAsync(cancellationToken); + return true; + } + public async Task<(IEnumerable Items, int TotalCount)> GetAddressbookPagedAsync( int page, int pageSize, diff --git a/SeaHaven.DataServices/Implementation/SettingsDataService.cs b/SeaHaven.DataServices/Implementation/SettingsDataService.cs new file mode 100644 index 0000000..f0a5feb --- /dev/null +++ b/SeaHaven.DataServices/Implementation/SettingsDataService.cs @@ -0,0 +1,125 @@ +using Data.SeaHavenIndustries; +using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Dto; +using SeaHaven.DataServices.Interfaces; + +namespace SeaHaven.DataServices.Implementation +{ + public class SettingsDataService : ISettingsDataService + { + private readonly ApplicationDbContext _context; + + public SettingsDataService(ApplicationDbContext context) + { + _context = context; + } + + public async Task AddCategoryAsync(string name, CancellationToken cancellationToken) + { + var category = new Category { Name = name }; + await _context.Categories.AddAsync(category, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task UpdateCategoryAsync(int id, string name, CancellationToken cancellationToken) + { + var exist = await _context.Categories + .Where(w => w.Id == id) + .FirstOrDefaultAsync(cancellationToken); + if (exist == null) + return false; + + exist.Name = name; + _context.Categories.Update(exist); + await _context.SaveChangesAsync(cancellationToken); + return true; + } + + public async Task DeleteCategoryCascadeAsync(int id, CancellationToken cancellationToken) + { + var exist = await _context.Categories + .Where(w => w.Id == id) + .FirstOrDefaultAsync(cancellationToken); + if (exist == null) + return false; + + var templates = await _context.Templates.Where(w => w.CategoryId == id).ToListAsync(cancellationToken); + if (templates != null) + { + _context.RemoveRange(templates); + } + + var workOrderCategories = await _context.workOrderCategories.Where(w => w.CategoryId == id).ToListAsync(cancellationToken); + if (workOrderCategories != null) + { + _context.RemoveRange(workOrderCategories); + } + + _context.Remove(exist); + await _context.SaveChangesAsync(cancellationToken); + return true; + } + + public async Task<(IReadOnlyList Items, int Total)> GetCategoryListPagedAsync( + int page, + int pageSize, + CancellationToken cancellationToken) + { + var totalCount = await _context.Categories.CountAsync(cancellationToken); + var data = await _context.Categories + .AsNoTracking() + .Select(s => new SettingsCategoryItemDTO + { + Id = s.Id, + Name = s.Name + }) + .OrderByDescending(d => d.Id) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return (data, totalCount); + } + + public async Task<(IReadOnlyList Items, int Total)> GetTemplatesPagedAsync( + int page, + int pageSize, + CancellationToken cancellationToken) + { + var totalCount = await _context.workOrders.CountAsync(w => w.istemplate == true, cancellationToken); + var data = await _context.workOrders + .AsNoTracking() + .Where(w => w.istemplate == true) + .OrderByDescending(d => d.Id) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return (data, totalCount); + } + + public async Task<(IReadOnlyList Users, int Total)> GetUsersPagedAsync( + string? search, + int page, + int pageSize, + CancellationToken cancellationToken) + { + var totalCount = await _context.Users.CountAsync(cancellationToken); + var users = await _context.Users + .AsNoTracking() + .Where(w => (w.FirstName ?? "").ToLower().Contains((search ?? "").ToLower())) + .OrderByDescending(d => d.Id) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return (users, totalCount); + } + + public async Task> GetRoleNamesAsync(CancellationToken cancellationToken) + { + var roles = await _context.Roles.AsNoTracking().ToListAsync(cancellationToken); + return roles.Select(r => r.Name!).ToList(); + } + } +} diff --git a/SeaHaven.DataServices/Interfaces/IDispatchDataService.cs b/SeaHaven.DataServices/Interfaces/IDispatchDataService.cs index 52473ba..050de10 100644 --- a/SeaHaven.DataServices/Interfaces/IDispatchDataService.cs +++ b/SeaHaven.DataServices/Interfaces/IDispatchDataService.cs @@ -1,4 +1,5 @@ using Data.SeaHavenIndustries; +using SeaHaven.DataServices.Dto; namespace SeaHaven.DataServices.Interfaces { @@ -28,5 +29,8 @@ namespace SeaHaven.DataServices.Interfaces Task HasSignoffTypeAsync(int dispatchId, string signoffType); Task> GetDispatchWorkOrdersAsync(int dispatchId); Task AddDispatchWorkOrderAsync(DispatchWorkOrder dispatchWorkOrder); + + Task GetListAsync(DispatchListQuery query, CancellationToken cancellationToken); + Task GetDetailAsync(int id, CancellationToken cancellationToken); } } diff --git a/SeaHaven.DataServices/Interfaces/IFollowUpDataService.cs b/SeaHaven.DataServices/Interfaces/IFollowUpDataService.cs index c8b8df3..6a6c5de 100644 --- a/SeaHaven.DataServices/Interfaces/IFollowUpDataService.cs +++ b/SeaHaven.DataServices/Interfaces/IFollowUpDataService.cs @@ -15,5 +15,15 @@ namespace SeaHaven.DataServices.Interfaces Task DeleteAsync(int id); Task ExistsAsync(int id); Task CountAsync(); + + Task> GetListPagedAsync(int page, int pageSize, string? search, string? status, CancellationToken cancellationToken); + Task GetListTotalAsync(string? search, string? status, CancellationToken cancellationToken); + Task GetDetailByIdAsync(int id, CancellationToken cancellationToken); + Task GetByIdForUpdateAsync(int id, CancellationToken cancellationToken); + Task GetUserNameAsync(string? userId, CancellationToken cancellationToken); + Task AddAsync(FollowUps followUp, CancellationToken cancellationToken); + Task UpdateAsync(FollowUps followUp, CancellationToken cancellationToken); + Task SetStatusAsync(int id, string status, CancellationToken cancellationToken); + Task RemoveByIdAsync(int id, CancellationToken cancellationToken); } } diff --git a/SeaHaven.DataServices/Interfaces/ILocationDataService.cs b/SeaHaven.DataServices/Interfaces/ILocationDataService.cs index 2865e42..d3ee23e 100644 --- a/SeaHaven.DataServices/Interfaces/ILocationDataService.cs +++ b/SeaHaven.DataServices/Interfaces/ILocationDataService.cs @@ -20,5 +20,12 @@ namespace SeaHaven.DataServices.Interfaces Task DeleteAsync(int id); Task ExistsAsync(int id); Task CountAsync(); + + Task<(List Items, int TotalCount)> GetListPagedAsync(int page, int pageSize, string? search, CancellationToken cancellationToken); + Task GetDetailByIdAsync(int id, CancellationToken cancellationToken); + Task GetByIdForUpdateAsync(int id, CancellationToken cancellationToken); + Task AddAsync(Locations location, CancellationToken cancellationToken); + Task UpdateAsync(Locations location, CancellationToken cancellationToken); + Task DeleteByIdAsync(int id, CancellationToken cancellationToken); } } diff --git a/SeaHaven.DataServices/Interfaces/ISettingsDataService.cs b/SeaHaven.DataServices/Interfaces/ISettingsDataService.cs new file mode 100644 index 0000000..78832a3 --- /dev/null +++ b/SeaHaven.DataServices/Interfaces/ISettingsDataService.cs @@ -0,0 +1,16 @@ +using Data.SeaHavenIndustries; +using SeaHaven.DataServices.Dto; + +namespace SeaHaven.DataServices.Interfaces +{ + public interface ISettingsDataService + { + Task AddCategoryAsync(string name, CancellationToken cancellationToken); + Task UpdateCategoryAsync(int id, string name, CancellationToken cancellationToken); + Task DeleteCategoryCascadeAsync(int id, CancellationToken cancellationToken); + Task<(IReadOnlyList Items, int Total)> GetCategoryListPagedAsync(int page, int pageSize, CancellationToken cancellationToken); + Task<(IReadOnlyList Items, int Total)> GetTemplatesPagedAsync(int page, int pageSize, CancellationToken cancellationToken); + Task<(IReadOnlyList Users, int Total)> GetUsersPagedAsync(string? search, int page, int pageSize, CancellationToken cancellationToken); + Task> GetRoleNamesAsync(CancellationToken cancellationToken); + } +} diff --git a/SeaHaven.Services/DTOs/DispatchDTOs.cs b/SeaHaven.Services/DTOs/DispatchDTOs.cs index 49a4a53..1a68b6c 100644 --- a/SeaHaven.Services/DTOs/DispatchDTOs.cs +++ b/SeaHaven.Services/DTOs/DispatchDTOs.cs @@ -27,4 +27,158 @@ namespace SeaHaven.Services.DTOs public DateTime? CompletedDate { get; set; } public string? Status { get; set; } } + + public class DispatchListEntryDTO + { + public int Id { get; set; } + public string? DispatchNumber { get; set; } + public string? PONumber { get; set; } + public decimal? NTEAmount { get; set; } + public string? Status { get; set; } + public DateTime? DispatchedAt { get; set; } + public DateTime? ScheduledDate { get; set; } + public DateTime? CompletedDate { get; set; } + public DateTime? AcknowledgedAt { get; set; } + public DateTime? VerifiedAt { get; set; } + public int? VendorId { get; set; } + public string? VendorName { get; set; } + public int? WorkOrderId { get; set; } + public string? PrimaryWOInternalNumber { get; set; } + public string? PrimaryWOTitle { get; set; } + public string? LocationName { get; set; } + public int WorkOrderCount { get; set; } + public int PendingUpliftCount { get; set; } + } + + public class DispatchListResultDTO + { + public IReadOnlyList Items { get; set; } = Array.Empty(); + public int Total { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + } + + public class DispatchDetailVendorResultDTO + { + public int Id { get; set; } + public string? CompanyName { get; set; } + public string? ContactName { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public string? Address { get; set; } + public string? City { get; set; } + public string? State { get; set; } + public string? Zip { get; set; } + public string? TradeSpecialties { get; set; } + } + + public class DispatchDetailLocationResultDTO + { + public int Id { get; set; } + public string? Name { get; set; } + public string? Address1 { get; set; } + public string? City { get; set; } + public string? State { get; set; } + public string? Zip { get; set; } + } + + public class DispatchDetailLinkedWorkOrderDTO + { + public int WorkOrderId { get; set; } + public string? InternalWONumber { get; set; } + public string? Title { get; set; } + public string? Priority { get; set; } + public string? Status { get; set; } + public string? LocationName { get; set; } + public string? Trade { get; set; } + public string? SubTrade { get; set; } + public string? Problem { get; set; } + } + + public class DispatchDetailChecklistDTO + { + 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 DispatchDetailSignoffDTO + { + 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 DispatchDetailCommentDTO + { + public int Id { get; set; } + public string? Commenttext { get; set; } + public string? Commenter { get; set; } + public string? CommentType { get; set; } + public string? RecordType { get; set; } + public DateTime? CreatedDate { get; set; } + public string? UserId { get; set; } + public string UserName { get; set; } = ""; + } + + public class DispatchDetailUpliftDTO + { + public int Id { get; set; } + public decimal? CurrentNTE { get; set; } + public decimal RequestedNTE { get; set; } + public decimal Delta { get; set; } + public string? VendorReason { get; set; } + public string? Status { get; set; } + public int RequiredTier { get; set; } + public string? RequestedByVendorName { get; set; } + public DateTime? RequestedAt { get; set; } + public DateTime? DecidedAt { get; set; } + public string? DecisionNote { get; set; } + public string DecidedByName { get; set; } = ""; + } + + public class DispatchDetailAuditLogDTO + { + public int Id { get; set; } + public string? Action { get; set; } + public string? FieldName { get; set; } + public string? OldValue { get; set; } + public string? NewValue { get; set; } + public DateTime? CreatedAt { get; set; } + public string? UserId { get; set; } + public string UserName { get; set; } = ""; + } + + public class DispatchDetailResultDTO + { + 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 DateTime? AcknowledgedAt { get; set; } + public DateTime? VerifiedAt { get; set; } + public string? VerifiedBy { get; set; } + public bool EmailSent { get; set; } + public int? WorkOrderId { get; set; } + public DispatchDetailVendorResultDTO? Vendor { get; set; } + public DispatchDetailLocationResultDTO? PrimaryLocation { get; set; } + public IReadOnlyList WorkOrders { get; set; } = Array.Empty(); + public IReadOnlyList Checklist { get; set; } = Array.Empty(); + public IReadOnlyList Signoffs { get; set; } = Array.Empty(); + public IReadOnlyList Comments { get; set; } = Array.Empty(); + public IReadOnlyList UpliftRequests { get; set; } = Array.Empty(); + public IReadOnlyList AuditLog { get; set; } = Array.Empty(); + } } diff --git a/SeaHaven.Services/DTOs/FollowUpDTOs.cs b/SeaHaven.Services/DTOs/FollowUpDTOs.cs index 4200416..1575296 100644 --- a/SeaHaven.Services/DTOs/FollowUpDTOs.cs +++ b/SeaHaven.Services/DTOs/FollowUpDTOs.cs @@ -40,4 +40,69 @@ namespace SeaHaven.Services.DTOs public int? AccountId { get; set; } public int? LocationId { get; set; } } + + public class FollowUpCreateRequestDTO + { + public string? Reason { get; set; } + public DateTime ScheduleDate { get; set; } + public TimeSpan ScheduleStartTime { get; set; } + public int? EmployeeId { get; set; } + public int? WorkOrderId { get; set; } + public int? AccountId { get; set; } + public int? LocationId { get; set; } + } + + public class FollowUpUpdateRequestDTO + { + public int Id { get; set; } + public string? Reason { get; set; } + public DateTime ScheduleDate { get; set; } + public TimeSpan ScheduleStartTime { get; set; } + public int? EmployeeId { get; set; } + public int? WorkOrderId { get; set; } + public int? AccountId { get; set; } + public int? LocationId { get; set; } + } + + public class FollowUpListRowDTO + { + public int Id { get; set; } + public string? Reason { get; set; } + public DateTime ScheduleDate { get; set; } + public TimeSpan ScheduleStartTime { get; set; } + public int? EmployeeId { get; set; } + public int? WorkOrderId { get; set; } + public int? AccountId { get; set; } + public int? LocationId { get; set; } + public string AccountName { get; set; } = ""; + public string AssignedTo { get; set; } = ""; + public string LocationName { get; set; } = ""; + public string WorkOrderNumber { get; set; } = ""; + public string InternalWONumber { get; set; } = ""; + public string? Status { get; set; } + public DateTime? CreatedDate { get; set; } + public string? CreatedById { get; set; } + public string CreatedByName { get; set; } = ""; + } + + public class FollowUpDetailDTO + { + public int Id { get; set; } + public string? Reason { get; set; } + public DateTime ScheduleDate { get; set; } + public TimeSpan ScheduleStartTime { get; set; } + public int? EmployeeId { get; set; } + public int? WorkOrderId { get; set; } + public int? AccountId { get; set; } + public int? LocationId { get; set; } + public string AccountName { get; set; } = ""; + public string AssignedTo { get; set; } = ""; + public string LocationName { get; set; } = ""; + public string WorkOrderNumber { get; set; } = ""; + public string InternalWONumber { get; set; } = ""; + public string? Status { get; set; } + public DateTime? CreatedDate { get; set; } + public string? CreatedById { get; set; } + public string CreatedByName { get; set; } = ""; + } } diff --git a/SeaHaven.Services/DTOs/LocationDTOs.cs b/SeaHaven.Services/DTOs/LocationDTOs.cs index 09e41b6..541ba14 100644 --- a/SeaHaven.Services/DTOs/LocationDTOs.cs +++ b/SeaHaven.Services/DTOs/LocationDTOs.cs @@ -36,4 +36,30 @@ namespace SeaHaven.Services.DTOs public string? Zipcode { get; set; } public int? AccountId { get; set; } } + + public class LocationCreateRequestDTO + { + public string? Name { get; set; } + public string? Title { get; set; } + public string? Address { get; set; } + public string? City { get; set; } + public string? State { get; set; } + public string? ZipCode { get; set; } + public string? Phone { get; set; } + public string? ContactEmail { get; set; } + public string? Status { get; set; } + } + + public class LocationUpdateRequestDTO + { + public string? Name { get; set; } + public string? Title { get; set; } + public string? Address { get; set; } + public string? City { get; set; } + public string? State { get; set; } + public string? ZipCode { get; set; } + public string? Phone { get; set; } + public string? ContactEmail { get; set; } + public string? Status { get; set; } + } } diff --git a/SeaHaven.Services/DTOs/SettingsDTOs.cs b/SeaHaven.Services/DTOs/SettingsDTOs.cs new file mode 100644 index 0000000..f538dda --- /dev/null +++ b/SeaHaven.Services/DTOs/SettingsDTOs.cs @@ -0,0 +1,98 @@ +using Data.SeaHavenIndustries.Enums; + +namespace SeaHaven.Services.DTOs +{ + public class RoleDTO + { + public string? Name { get; set; } + public string? Description { get; set; } + } + + public class SettingsUserDTO + { + public string? Id { get; set; } + public string? UserName { get; set; } + public string? Email { get; set; } + public string? RoleName { get; set; } + public string? Status { get; set; } + public string? Date { get; set; } + } + + public class SettingsCategoryDTO + { + public int Id { get; set; } + public string? Name { get; set; } + } + + public class SettingsTemplateDTO + { + public int Id { get; set; } + public bool? IsDeleted { get; set; } + public string? createdby { get; set; } + public string? DeleterUserId { get; set; } + public DateTime? DeletionTime { get; set; } + public DateTime? CreatedDate { get; set; } + public DateTime? LastModificationTime { get; set; } + public int? LastModifierUserId { get; set; } + + public string? InternalWONumber { get; set; } + public string? ExternalWorkOrderId { get; set; } + public string? WorkerOrderNumber { get; set; } + public string? WorkerOrderTitle { get; set; } + public string? Description { get; set; } + public string? Customer { get; set; } + public string? SiteCode { get; set; } + public string? Building { get; set; } + public string? Severity { get; set; } + public DateTime? DueDate { get; set; } + public string? AssignTo { get; set; } + public int? LocationId { get; set; } + public LifecycleStatus? LifecycleStatus { get; set; } + public string? LegacyStatus { get; set; } + public WorkOrderType? WorkOrderType { get; set; } + public string? Service { get; set; } + public int? PrimaryDispatchId { get; set; } + + public DateTime? ScheduledDate { get; set; } + public DateTime? ScheduledStart { get; set; } + public DateTime? ScheduledEnd { get; set; } + public DateOnly? TargetWeek { get; set; } + public bool? ScheduleWeekOnly { get; set; } + + public DateOnly? OriginalWeek { get; set; } + public DateOnly? OriginalDate { get; set; } + + public int RescheduleCount { get; set; } + public int CarriedOver { get; set; } + public OperationalFlags OperationalFlags { get; set; } + + public DocStatus? DocStatus { get; set; } + + public string? AvettaTask { get; set; } + public DateOnly? AssignDate { get; set; } + + public DateTime? DateReported { get; set; } + public string? SourceEmailS3Key { get; set; } + public string? Problem { get; set; } + public string? Trade { get; set; } + public string? SubTrade { get; set; } + public decimal? VendorNTE { get; set; } + public DateTime? CompletedDate { get; set; } + public string? Source { get; set; } + public bool? istemplate { get; set; } + public string? PO { get; set; } + public string? TT { get; set; } + public string? Priority { get; set; } + public string? Status { get; set; } + public string? Attachments { get; set; } + 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 byte[]? RowVersion { get; set; } + } +} diff --git a/SeaHaven.Services/Implementation/DispatchService.cs b/SeaHaven.Services/Implementation/DispatchService.cs index 2f2d200..680dbca 100644 --- a/SeaHaven.Services/Implementation/DispatchService.cs +++ b/SeaHaven.Services/Implementation/DispatchService.cs @@ -1,5 +1,6 @@ using Data.SeaHavenIndustries; using FluentValidation; +using SeaHaven.DataServices.Dto; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.DTOs; using SeaHaven.Services.Interfaces; @@ -141,6 +142,184 @@ namespace SeaHaven.Services.Implementation return await _dispatchDataService.CountAsync(); } + public async Task GetDispatchListAsync( + string? search, + string? status, + int? vendorId, + DateTime? dateFrom, + DateTime? dateTo, + string? sortBy, + string? sortDir, + int page, + int pageSize, + CancellationToken cancellationToken) + { + var query = new DispatchListQuery + { + Search = search, + Status = status, + VendorId = vendorId, + DateFrom = dateFrom, + DateTo = dateTo, + SortBy = sortBy, + SortDir = sortDir, + Page = page, + PageSize = pageSize + }; + + var result = await _dispatchDataService.GetListAsync(query, cancellationToken); + + return new DispatchListResultDTO + { + Items = result.Items.Select(MapListEntry).ToList(), + Total = result.Total, + Page = result.Page, + PageSize = result.PageSize + }; + } + + public async Task GetDispatchDetailAsync(int id, CancellationToken cancellationToken) + { + var detail = await _dispatchDataService.GetDetailAsync(id, cancellationToken); + return detail == null ? null : MapDetail(detail); + } + + private static DispatchListEntryDTO MapListEntry(DispatchListItemDTO d) + { + return new DispatchListEntryDTO + { + Id = d.Id, + DispatchNumber = d.DispatchNumber, + PONumber = d.PONumber, + NTEAmount = d.NTEAmount, + Status = d.Status, + DispatchedAt = d.DispatchedAt, + ScheduledDate = d.ScheduledDate, + CompletedDate = d.CompletedDate, + AcknowledgedAt = d.AcknowledgedAt, + VerifiedAt = d.VerifiedAt, + VendorId = d.VendorId, + VendorName = d.VendorName, + WorkOrderId = d.WorkOrderId, + PrimaryWOInternalNumber = d.PrimaryWOInternalNumber, + PrimaryWOTitle = d.PrimaryWOTitle, + LocationName = d.LocationName, + WorkOrderCount = d.WorkOrderCount, + PendingUpliftCount = d.PendingUpliftCount + }; + } + + private static DispatchDetailResultDTO MapDetail(DispatchDetailDTO d) + { + return new DispatchDetailResultDTO + { + 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, + AcknowledgedAt = d.AcknowledgedAt, + VerifiedAt = d.VerifiedAt, + VerifiedBy = d.VerifiedBy, + EmailSent = d.EmailSent, + WorkOrderId = d.WorkOrderId, + Vendor = d.Vendor == null ? null : new DispatchDetailVendorResultDTO + { + Id = d.Vendor.Id, + CompanyName = d.Vendor.CompanyName, + ContactName = d.Vendor.ContactName, + Email = d.Vendor.Email, + Phone = d.Vendor.Phone, + Address = d.Vendor.Address, + City = d.Vendor.City, + State = d.Vendor.State, + Zip = d.Vendor.Zip, + TradeSpecialties = d.Vendor.TradeSpecialties + }, + PrimaryLocation = d.PrimaryLocation == null ? null : new DispatchDetailLocationResultDTO + { + Id = d.PrimaryLocation.Id, + Name = d.PrimaryLocation.Name, + Address1 = d.PrimaryLocation.Address1, + City = d.PrimaryLocation.City, + State = d.PrimaryLocation.State, + Zip = d.PrimaryLocation.Zip + }, + WorkOrders = d.WorkOrders.Select(w => new DispatchDetailLinkedWorkOrderDTO + { + WorkOrderId = w.WorkOrderId, + InternalWONumber = w.InternalWONumber, + Title = w.Title, + Priority = w.Priority, + Status = w.Status, + LocationName = w.LocationName, + Trade = w.Trade, + SubTrade = w.SubTrade, + Problem = w.Problem + }).ToList(), + Checklist = d.Checklist.Select(c => new DispatchDetailChecklistDTO + { + Id = c.Id, + ItemText = c.ItemText, + IsCompleted = c.IsCompleted, + CompletedBy = c.CompletedBy, + CompletedAt = c.CompletedAt, + SortOrder = c.SortOrder, + WorkOrderId = c.WorkOrderId + }).ToList(), + Signoffs = d.Signoffs.Select(s => new DispatchDetailSignoffDTO + { + Id = s.Id, + SignoffType = s.SignoffType, + Name = s.Name, + Signature = s.Signature, + SignatureMethod = s.SignatureMethod, + SignedAt = s.SignedAt + }).ToList(), + Comments = d.Comments.Select(c => new DispatchDetailCommentDTO + { + Id = c.Id, + Commenttext = c.Commenttext, + Commenter = c.Commenter, + CommentType = c.CommentType, + RecordType = c.RecordType, + CreatedDate = c.CreatedDate, + UserId = c.UserId, + UserName = c.UserName + }).ToList(), + UpliftRequests = d.UpliftRequests.Select(u => new DispatchDetailUpliftDTO + { + Id = u.Id, + CurrentNTE = u.CurrentNTE, + RequestedNTE = u.RequestedNTE, + Delta = u.Delta, + VendorReason = u.VendorReason, + Status = u.Status, + RequiredTier = u.RequiredTier, + RequestedByVendorName = u.RequestedByVendorName, + RequestedAt = u.RequestedAt, + DecidedAt = u.DecidedAt, + DecisionNote = u.DecisionNote, + DecidedByName = u.DecidedByName + }).ToList(), + AuditLog = d.AuditLog.Select(a => new DispatchDetailAuditLogDTO + { + Id = a.Id, + Action = a.Action, + FieldName = a.FieldName, + OldValue = a.OldValue, + NewValue = a.NewValue, + CreatedAt = a.CreatedAt, + UserId = a.UserId, + UserName = a.UserName + }).ToList() + }; + } + private DispatchDTO MapToDTO(Dispatch dispatch) { return new DispatchDTO diff --git a/SeaHaven.Services/Implementation/FollowUpService.cs b/SeaHaven.Services/Implementation/FollowUpService.cs index e95c517..7331001 100644 --- a/SeaHaven.Services/Implementation/FollowUpService.cs +++ b/SeaHaven.Services/Implementation/FollowUpService.cs @@ -148,6 +148,131 @@ namespace SeaHaven.Services.Implementation return await _followUpDataService.CountAsync(); } + public async Task> GetFollowUpListAsync( + int page, + int pageSize, + string? search, + string? status, + CancellationToken cancellationToken) + { + status = string.IsNullOrWhiteSpace(status) ? "Open" : status; + + var items = await _followUpDataService.GetListPagedAsync(page, pageSize, search, status, cancellationToken); + var totalCount = await _followUpDataService.GetListTotalAsync(search, status, cancellationToken); + + return new PagedResult + { + Items = items.Select(MapToListRow).ToList(), + TotalCount = totalCount, + Page = page, + PageSize = pageSize + }; + } + + public async Task GetFollowUpDetailAsync(int id, CancellationToken cancellationToken) + { + var followUp = await _followUpDataService.GetDetailByIdAsync(id, cancellationToken); + if (followUp == null) + return null; + + var createdByName = await _followUpDataService.GetUserNameAsync(followUp.createdby, cancellationToken); + + return new FollowUpDetailDTO + { + Id = followUp.Id, + Reason = followUp.Reason, + ScheduleDate = followUp.ScheduleDate, + ScheduleStartTime = followUp.ScheduleStartTime, + EmployeeId = followUp.EmployeeId, + WorkOrderId = followUp.WorkOrderId, + AccountId = followUp.AccountId, + LocationId = followUp.LocationId, + AccountName = followUp.Account?.Name ?? "", + AssignedTo = followUp.Employee != null + ? $"{followUp.Employee.FirstName} {followUp.Employee.LastName}" + : "", + LocationName = followUp.Location?.Name ?? "", + WorkOrderNumber = "", + InternalWONumber = "", + Status = followUp.Status, + CreatedDate = followUp.CreatedDate, + CreatedById = followUp.createdby, + CreatedByName = createdByName + }; + } + + public async Task CreateOpenFollowUpAsync(FollowUpCreateRequestDTO dto, string userId, CancellationToken cancellationToken) + { + var followUp = new FollowUps + { + Reason = dto.Reason, + ScheduleDate = dto.ScheduleDate, + ScheduleStartTime = dto.ScheduleStartTime, + EmployeeId = dto.EmployeeId, + WorkOrderId = dto.WorkOrderId, + AccountId = dto.AccountId, + LocationId = dto.LocationId, + Status = "Open", + createdby = userId + }; + + var created = await _followUpDataService.AddAsync(followUp, cancellationToken); + return created.Id; + } + + public async Task UpdateFollowUpFieldsAsync(int id, FollowUpUpdateRequestDTO dto, CancellationToken cancellationToken) + { + var followUp = await _followUpDataService.GetByIdForUpdateAsync(id, cancellationToken); + if (followUp == null) + throw new KeyNotFoundException($"Follow-Up with ID {id} not found"); + + followUp.Reason = dto.Reason; + followUp.ScheduleDate = dto.ScheduleDate; + followUp.ScheduleStartTime = dto.ScheduleStartTime; + followUp.EmployeeId = dto.EmployeeId; + followUp.WorkOrderId = dto.WorkOrderId; + followUp.AccountId = dto.AccountId; + followUp.LocationId = dto.LocationId; + + await _followUpDataService.UpdateAsync(followUp, cancellationToken); + } + + public Task SetFollowUpStatusAsync(int id, string status, CancellationToken cancellationToken) + { + return _followUpDataService.SetStatusAsync(id, status, cancellationToken); + } + + public Task RemoveFollowUpAsync(int id, CancellationToken cancellationToken) + { + return _followUpDataService.RemoveByIdAsync(id, cancellationToken); + } + + private static FollowUpListRowDTO MapToListRow(FollowUps followUp) + { + return new FollowUpListRowDTO + { + Id = followUp.Id, + Reason = followUp.Reason, + ScheduleDate = followUp.ScheduleDate, + ScheduleStartTime = followUp.ScheduleStartTime, + EmployeeId = followUp.EmployeeId, + WorkOrderId = followUp.WorkOrderId, + AccountId = followUp.AccountId, + LocationId = followUp.LocationId, + AccountName = followUp.Account?.Name ?? "", + AssignedTo = followUp.Employee != null + ? $"{followUp.Employee.FirstName} {followUp.Employee.LastName}" + : "", + LocationName = followUp.Location?.Name ?? "", + WorkOrderNumber = "", + InternalWONumber = "", + Status = followUp.Status, + CreatedDate = followUp.CreatedDate, + CreatedById = null, + CreatedByName = "" + }; + } + private FollowUpDTO MapToDTO(FollowUps followUp) { return new FollowUpDTO diff --git a/SeaHaven.Services/Implementation/LocationService.cs b/SeaHaven.Services/Implementation/LocationService.cs index c625192..7edd2c4 100644 --- a/SeaHaven.Services/Implementation/LocationService.cs +++ b/SeaHaven.Services/Implementation/LocationService.cs @@ -139,6 +139,70 @@ namespace SeaHaven.Services.Implementation return await _locationDataService.CountAsync(); } + public async Task> GetLocationListPagedAsync( + int page, + int pageSize, + string? search, + CancellationToken cancellationToken) + { + var (items, totalCount) = await _locationDataService.GetListPagedAsync(page, pageSize, search, cancellationToken); + return new PagedResult + { + Items = items.Select(MapToDTO).ToList(), + TotalCount = totalCount, + Page = page, + PageSize = pageSize + }; + } + + public async Task GetLocationDetailAsync(int id, CancellationToken cancellationToken) + { + var location = await _locationDataService.GetDetailByIdAsync(id, cancellationToken); + return location == null ? null : MapToDTO(location); + } + + public async Task CreateLocationFromRequestAsync(LocationCreateRequestDTO request, CancellationToken cancellationToken) + { + var location = new Locations + { + Name = request.Name, + Title = request.Title, + Address1 = request.Address, + City = request.City, + State = request.State, + Zip = request.ZipCode, + PhoneNumber = request.Phone, + Email = request.ContactEmail, + Status = request.Status + }; + + await _locationDataService.AddAsync(location, cancellationToken); + } + + public async Task UpdateLocationFromRequestAsync(int id, LocationUpdateRequestDTO request, CancellationToken cancellationToken) + { + var location = await _locationDataService.GetByIdForUpdateAsync(id, cancellationToken); + if (location == null) + throw new KeyNotFoundException($"Location with ID {id} not found"); + + location.Name = request.Name; + location.Title = request.Title; + location.Address1 = request.Address; + location.City = request.City; + location.State = request.State; + location.Zip = request.ZipCode; + location.PhoneNumber = request.Phone; + location.Email = request.ContactEmail; + location.Status = request.Status; + + await _locationDataService.UpdateAsync(location, cancellationToken); + } + + public Task DeleteLocationByIdAsync(int id, CancellationToken cancellationToken) + { + return _locationDataService.DeleteByIdAsync(id, cancellationToken); + } + // Private helper methods private LocationDTO MapToDTO(Locations location) { diff --git a/SeaHaven.Services/Implementation/SettingsService.cs b/SeaHaven.Services/Implementation/SettingsService.cs new file mode 100644 index 0000000..c513f88 --- /dev/null +++ b/SeaHaven.Services/Implementation/SettingsService.cs @@ -0,0 +1,166 @@ +using Data.SeaHavenIndustries; +using Microsoft.AspNetCore.Identity; +using SeaHaven.DataServices.Dto; +using SeaHaven.DataServices.Interfaces; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Interfaces; + +namespace SeaHaven.Services.Implementation +{ + public class SettingsService : ISettingsService + { + private readonly ISettingsDataService _dataService; + private readonly UserManager _userManager; + + public SettingsService(ISettingsDataService dataService, UserManager userManager) + { + _dataService = dataService; + _userManager = userManager; + } + + public async Task AddCategoryAsync(string? name, CancellationToken cancellationToken) + { + await _dataService.AddCategoryAsync(name ?? "", cancellationToken); + } + + public Task UpdateCategoryAsync(int? id, string? name, CancellationToken cancellationToken) + { + return _dataService.UpdateCategoryAsync(id ?? 0, name ?? "", cancellationToken); + } + + public Task DeleteCategoryAsync(int id, CancellationToken cancellationToken) + { + return _dataService.DeleteCategoryCascadeAsync(id, cancellationToken); + } + + public async Task<(IReadOnlyList Items, int Total)> GetCategoryListAsync(int page, int pageSize, CancellationToken cancellationToken) + { + var (items, total) = await _dataService.GetCategoryListPagedAsync(page, pageSize, cancellationToken); + return (items.Select(c => new SettingsCategoryDTO { Id = c.Id, Name = c.Name }).ToList(), total); + } + + public async Task<(IReadOnlyList Items, int Total)> GetTemplatesAsync(int page, int pageSize, CancellationToken cancellationToken) + { + var (items, total) = await _dataService.GetTemplatesPagedAsync(page, pageSize, cancellationToken); + return (items.Select(MapTemplate).ToList(), total); + } + + public async Task<(IReadOnlyList Users, int Total)> GetUsersAsync(string? search, int page, int pageSize, CancellationToken cancellationToken) + { + var (users, total) = await _dataService.GetUsersPagedAsync(search, page, pageSize, cancellationToken); + + var filtered = new List(); + foreach (var user in users) + { + var roles = await _userManager.GetRolesAsync(user); + if (!roles.Contains("Admin")) + { + filtered.Add(new SettingsUserDTO + { + RoleName = roles.Count() > 0 ? roles.First() : "", + Email = user.Email, + UserName = user.FirstName, + Date = user.CreatedDate?.Date.ToString(), + Status = user.UniqueName, + Id = user.Id + }); + } + } + + return (filtered, total); + } + + public async Task> GetRolesAsync(CancellationToken cancellationToken) + { + var names = await _dataService.GetRoleNamesAsync(cancellationToken); + var roles = new List(); + foreach (var name in names) + { + if (name == "Admin") + { + roles.Add(new RoleDTO { Name = name, Description = "Full Access" }); + } + else if (name == "Supervisor") + { + roles.Add(new RoleDTO { Name = name, Description = "Access to Team's Work Orders Only" }); + } + else if (name == "Manager") + { + roles.Add(new RoleDTO { Name = name, Description = "Full access accept quotes" }); + } + else if (name == "User") + { + roles.Add(new RoleDTO { Name = name, Description = "Access to Assigned Work Orders Only" }); + } + } + return roles; + } + + private static SettingsTemplateDTO MapTemplate(WorkOrder w) + { + return new SettingsTemplateDTO + { + Id = w.Id, + IsDeleted = w.IsDeleted, + createdby = w.createdby, + DeleterUserId = w.DeleterUserId, + DeletionTime = w.DeletionTime, + CreatedDate = w.CreatedDate, + LastModificationTime = w.LastModificationTime, + LastModifierUserId = w.LastModifierUserId, + InternalWONumber = w.InternalWONumber, + ExternalWorkOrderId = w.ExternalWorkOrderId, + WorkerOrderNumber = w.WorkerOrderNumber, + WorkerOrderTitle = w.WorkerOrderTitle, + Description = w.Description, + Customer = w.Customer, + SiteCode = w.SiteCode, + Building = w.Building, + Severity = w.Severity, + DueDate = w.DueDate, + AssignTo = w.AssignTo, + LocationId = w.LocationId, + LifecycleStatus = w.LifecycleStatus, + LegacyStatus = w.LegacyStatus, + WorkOrderType = w.WorkOrderType, + Service = w.Service, + PrimaryDispatchId = w.PrimaryDispatchId, + ScheduledDate = w.ScheduledDate, + ScheduledStart = w.ScheduledStart, + ScheduledEnd = w.ScheduledEnd, + TargetWeek = w.TargetWeek, + ScheduleWeekOnly = w.ScheduleWeekOnly, + OriginalWeek = w.OriginalWeek, + OriginalDate = w.OriginalDate, + RescheduleCount = w.RescheduleCount, + CarriedOver = w.CarriedOver, + OperationalFlags = w.OperationalFlags, + DocStatus = w.DocStatus, + AvettaTask = w.AvettaTask, + AssignDate = w.AssignDate, + DateReported = w.DateReported, + SourceEmailS3Key = w.SourceEmailS3Key, + Problem = w.Problem, + Trade = w.Trade, + SubTrade = w.SubTrade, + VendorNTE = w.VendorNTE, + CompletedDate = w.CompletedDate, + Source = w.Source, + istemplate = w.istemplate, + PO = w.PO, + TT = w.TT, + Priority = w.Priority, + Status = w.Status, + Attachments = w.Attachments, + BeforPhotoIssue = w.BeforPhotoIssue, + BeforPhotoAttachment = w.BeforPhotoAttachment, + AfterPhotoIssue = w.AfterPhotoIssue, + AfterPhotoAttachment = w.AfterPhotoAttachment, + SignOffName = w.SignOffName, + SignOffAttachment = w.SignOffAttachment, + SignOffSignature = w.SignOffSignature, + RowVersion = w.RowVersion + }; + } + } +} diff --git a/SeaHaven.Services/Interfaces/IDispatchService.cs b/SeaHaven.Services/Interfaces/IDispatchService.cs index a650389..1f1da0a 100644 --- a/SeaHaven.Services/Interfaces/IDispatchService.cs +++ b/SeaHaven.Services/Interfaces/IDispatchService.cs @@ -15,5 +15,8 @@ namespace SeaHaven.Services.Interfaces Task DispatchExistsAsync(int id); Task GenerateNextDispatchNumberAsync(); Task GetTotalDispatchCountAsync(); + + Task GetDispatchListAsync(string? search, string? status, int? vendorId, DateTime? dateFrom, DateTime? dateTo, string? sortBy, string? sortDir, int page, int pageSize, CancellationToken cancellationToken); + Task GetDispatchDetailAsync(int id, CancellationToken cancellationToken); } } diff --git a/SeaHaven.Services/Interfaces/IFollowUpService.cs b/SeaHaven.Services/Interfaces/IFollowUpService.cs index ae0b3dd..0b2fd94 100644 --- a/SeaHaven.Services/Interfaces/IFollowUpService.cs +++ b/SeaHaven.Services/Interfaces/IFollowUpService.cs @@ -14,5 +14,12 @@ namespace SeaHaven.Services.Interfaces Task DeleteFollowUpAsync(int id, string userId); Task FollowUpExistsAsync(int id); Task GetTotalFollowUpCountAsync(); + + Task> GetFollowUpListAsync(int page, int pageSize, string? search, string? status, CancellationToken cancellationToken); + Task GetFollowUpDetailAsync(int id, CancellationToken cancellationToken); + Task CreateOpenFollowUpAsync(FollowUpCreateRequestDTO dto, string userId, CancellationToken cancellationToken); + Task UpdateFollowUpFieldsAsync(int id, FollowUpUpdateRequestDTO dto, CancellationToken cancellationToken); + Task SetFollowUpStatusAsync(int id, string status, CancellationToken cancellationToken); + Task RemoveFollowUpAsync(int id, CancellationToken cancellationToken); } } diff --git a/SeaHaven.Services/Interfaces/ILocationService.cs b/SeaHaven.Services/Interfaces/ILocationService.cs index 7bf9d4e..95db466 100644 --- a/SeaHaven.Services/Interfaces/ILocationService.cs +++ b/SeaHaven.Services/Interfaces/ILocationService.cs @@ -14,5 +14,11 @@ namespace SeaHaven.Services.Interfaces Task DeleteLocationAsync(int id, string userId); Task LocationExistsAsync(int id); Task GetTotalLocationCountAsync(); + + Task> GetLocationListPagedAsync(int page, int pageSize, string? search, CancellationToken cancellationToken); + Task GetLocationDetailAsync(int id, CancellationToken cancellationToken); + Task CreateLocationFromRequestAsync(LocationCreateRequestDTO request, CancellationToken cancellationToken); + Task UpdateLocationFromRequestAsync(int id, LocationUpdateRequestDTO request, CancellationToken cancellationToken); + Task DeleteLocationByIdAsync(int id, CancellationToken cancellationToken); } } diff --git a/SeaHaven.Services/Interfaces/ISettingsService.cs b/SeaHaven.Services/Interfaces/ISettingsService.cs new file mode 100644 index 0000000..6b920b8 --- /dev/null +++ b/SeaHaven.Services/Interfaces/ISettingsService.cs @@ -0,0 +1,15 @@ +using SeaHaven.Services.DTOs; + +namespace SeaHaven.Services.Interfaces +{ + public interface ISettingsService + { + Task AddCategoryAsync(string? name, CancellationToken cancellationToken); + Task UpdateCategoryAsync(int? id, string? name, CancellationToken cancellationToken); + Task DeleteCategoryAsync(int id, CancellationToken cancellationToken); + Task<(IReadOnlyList Items, int Total)> GetCategoryListAsync(int page, int pageSize, CancellationToken cancellationToken); + Task<(IReadOnlyList Items, int Total)> GetTemplatesAsync(int page, int pageSize, CancellationToken cancellationToken); + Task<(IReadOnlyList Users, int Total)> GetUsersAsync(string? search, int page, int pageSize, CancellationToken cancellationToken); + Task> GetRolesAsync(CancellationToken cancellationToken); + } +}