refactor(api): complete feature service boundaries

This commit is contained in:
Alexandre Brandizzi 2026-07-24 11:24:05 -03:00
parent aa59399b78
commit 445cd68468
35 changed files with 2993 additions and 610 deletions

View file

@ -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<IDispatchService> 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<IDispatchService>();
service.Setup(s => s.GetDispatchListAsync("a", null, null, null, null, "dispatchedAt", "desc", 1, 25, It.IsAny<CancellationToken>()))
.ReturnsAsync(new DispatchListResultDTO
{
Total = 2,
Page = 1,
PageSize = 25,
Items = new List<DispatchListEntryDTO>
{
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<OkObjectResult>().Subject;
var response = ok.Value.Should().BeOfType<DataResponse>().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<object>().Count().Should().Be(2);
}
[Fact]
public async Task GetById_WhenMissing_ReturnsNotFound()
{
var service = new Mock<IDispatchService>();
service.Setup(s => s.GetDispatchDetailAsync(99, It.IsAny<CancellationToken>()))
.ReturnsAsync((DispatchDetailResultDTO?)null);
var result = await NewController(service).GetById(99, CancellationToken.None);
var notFound = result.Should().BeOfType<NotFoundObjectResult>().Subject;
notFound.Value.Should().BeOfType<Response>().Subject.Message.Should().Be("Dispatch not found");
}
[Fact]
public async Task GetById_WhenFound_ReturnsDetailAsData()
{
var service = new Mock<IDispatchService>();
var detail = new DispatchDetailResultDTO { Id = 5, DispatchNumber = "D5", Status = "Pending" };
service.Setup(s => s.GetDispatchDetailAsync(5, It.IsAny<CancellationToken>()))
.ReturnsAsync(detail);
var result = await NewController(service).GetById(5, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var response = ok.Value.Should().BeOfType<DataResponse>().Subject;
response.Status.Should().Be("Success");
((object?)response.Data).Should().BeSameAs(detail);
}
}

View file

@ -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<IDispatchDataService> data) =>
new(data.Object, new CreateDispatchValidation(), new UpdateDispatchValidation());
[Fact]
public async Task GetDispatchListAsync_MapsDataReadModelToServiceDtoPreservingFields()
{
var data = new Mock<IDispatchDataService>();
var source = new DispatchListResult
{
Total = 1,
Page = 2,
PageSize = 25,
Items = new List<DispatchListItemDTO>
{
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<DispatchListQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(source);
var result = await NewService(data).GetDispatchListAsync("a", "Pending", 9, null, null, "ponumber", "asc", 2, 25, CancellationToken.None);
result.Should().BeOfType<DispatchListResultDTO>();
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<IDispatchDataService>();
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<DispatchLinkedWorkOrderDTO> { new() { WorkOrderId = 11, Title = "T", Priority = "High" } },
Checklist = new List<DispatchChecklistDTO> { new() { Id = 1, ItemText = "Check", IsCompleted = true, SortOrder = 1 } },
Signoffs = new List<DispatchSignoffDTO> { new() { Id = 1, SignoffType = "Vendor", Name = "J" } },
Comments = new List<DispatchCommentDTO> { new() { Id = 1, Commenttext = "Hi", UserName = "Sara" } },
UpliftRequests = new List<DispatchUpliftRequestDTO> { new() { Id = 1, RequestedNTE = 600m, CurrentNTE = 500m, Delta = 100m, Status = "Pending" } },
AuditLog = new List<DispatchAuditLogDTO> { new() { Id = 1, Action = "Updated", FieldName = "DISP-42:Status" } }
};
data.Setup(d => d.GetDetailAsync(42, It.IsAny<CancellationToken>())).ReturnsAsync(detail);
data.Setup(d => d.GetDetailAsync(999, It.IsAny<CancellationToken>())).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<DispatchDetailResultDTO>();
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");
}
}

View file

@ -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<IFollowUpService> 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<IFollowUpService> 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<IFollowUpService>();
service.Setup(s => s.GetFollowUpListAsync(1, 12, "", "Open", It.IsAny<CancellationToken>()))
.ReturnsAsync(new PagedResult<FollowUpListRowDTO>
{
Items = new List<FollowUpListRowDTO> { new() { Id = 1, Reason = "R", CreatedByName = "Me" } },
TotalCount = 1,
Page = 1,
PageSize = 12
});
var result = await NewController(service).GetFollowUps();
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<Pagination_DTO>().Subject;
envelope.PageSize.Should().Be(12);
envelope.TotalCount.Should().Be(1);
}
[Fact]
public async Task GetById_WhenMissing_ReturnsNotFound()
{
var service = new Mock<IFollowUpService>();
service.Setup(s => s.GetFollowUpDetailAsync(9, It.IsAny<CancellationToken>()))
.ReturnsAsync((FollowUpDetailDTO?)null);
var result = await NewController(service).GetById(9, CancellationToken.None);
result.Should().BeOfType<NotFoundResult>();
}
[Fact]
public async Task Create_WhenNoUser_ReturnsUnauthorized()
{
var service = new Mock<IFollowUpService>();
var result = await NewControllerNoUser(service).Create(new FollowUp_DTO { Reason = "x" }, CancellationToken.None);
var unauthorized = result.Should().BeOfType<UnauthorizedObjectResult>().Subject;
unauthorized.Value.Should().BeOfType<Response>().Subject.Message.Should().Be("User not authenticated");
service.Verify(s => s.CreateOpenFollowUpAsync(It.IsAny<FollowUpCreateRequestDTO>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task Create_WhenAuthenticated_ReturnsCreatedWithId()
{
var service = new Mock<IFollowUpService>();
service.Setup(s => s.CreateOpenFollowUpAsync(It.IsAny<FollowUpCreateRequestDTO>(), "user-1", It.IsAny<CancellationToken>()))
.ReturnsAsync(42);
var result = await NewController(service).Create(new FollowUp_DTO { Reason = "x" }, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var response = ok.Value.Should().BeOfType<DataResponse>().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<IFollowUpService>();
service.Setup(s => s.SetFollowUpStatusAsync(3, "Completed", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var result = await NewController(service).Complete(3, CancellationToken.None);
result.Should().BeOfType<NotFoundResult>();
}
[Fact]
public async Task Delete_WhenMissing_ReturnsNotFoundEnvelope()
{
var service = new Mock<IFollowUpService>();
service.Setup(s => s.RemoveFollowUpAsync(8, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var result = await NewController(service).Delete(8, CancellationToken.None);
var notFound = result.Should().BeOfType<NotFoundObjectResult>().Subject;
notFound.Value.Should().BeOfType<Response>().Subject.Message.Should().Be("Follow Up not found");
}
}

View file

@ -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<ApplicationDbContext>()
.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<KeyNotFoundException>();
}
[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();
}
}

View file

@ -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<ILocationService> 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<ILocationService>();
service.Setup(s => s.GetLocationListPagedAsync(1, 10, "a", It.IsAny<CancellationToken>()))
.ReturnsAsync(new PagedResult<LocationDTO>
{
Items = new List<LocationDTO> { 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<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<Pagination_DTO>().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<ILocationService>();
service.Setup(s => s.GetLocationDetailAsync(5, It.IsAny<CancellationToken>()))
.ReturnsAsync((LocationDTO?)null);
var result = await NewController(service).GetLocationById(5, CancellationToken.None);
var notFound = result.Should().BeOfType<NotFoundObjectResult>().Subject;
var response = notFound.Value.Should().BeOfType<Response>().Subject;
response.Message.Should().Be("Location not found");
}
[Fact]
public async Task GetLocationById_WhenFound_MapsServiceDtoToAliasedShape()
{
var service = new Mock<ILocationService>();
service.Setup(s => s.GetLocationDetailAsync(7, It.IsAny<CancellationToken>()))
.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<OkObjectResult>().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<ILocationService>();
var result = await NewController(service).AddLocation(new Location_DTO { Name = "X" }, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var response = ok.Value.Should().BeOfType<DataResponse>().Subject;
response.Status.Should().Be("200");
response.Message.Should().Be("Location Created Successfully");
service.Verify(s => s.CreateLocationFromRequestAsync(It.IsAny<LocationCreateRequestDTO>(), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task EditLocation_WhenMissing_ReturnsNotFound()
{
var service = new Mock<ILocationService>();
service.Setup(s => s.UpdateLocationFromRequestAsync(99, It.IsAny<LocationUpdateRequestDTO>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new KeyNotFoundException());
var result = await NewController(service).EditLocation(99, new EditLocation_DTO { Name = "X" }, CancellationToken.None);
var notFound = result.Should().BeOfType<NotFoundObjectResult>().Subject;
notFound.Value.Should().BeOfType<Response>().Subject.Message.Should().Be("Location not found");
}
}

View file

@ -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<ApplicationDbContext>()
.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<KeyNotFoundException>();
}
[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();
}
}

View file

@ -54,4 +54,26 @@ public class ServiceRegistrationTests
provider.GetRequiredService<IDashboardDataService>().Should().BeOfType<DashboardDataService>(); provider.GetRequiredService<IDashboardDataService>().Should().BeOfType<DashboardDataService>();
provider.GetRequiredService<IDropdownOptionsDataService>().Should().BeOfType<DropdownOptionsDataService>(); provider.GetRequiredService<IDropdownOptionsDataService>().Should().BeOfType<DropdownOptionsDataService>();
} }
[Fact]
public void ConventionDi_RegistersFollowUpLocationDispatchSettingsServices()
{
using var provider = BuildProvider();
provider.GetRequiredService<IFollowUpService>().Should().BeOfType<FollowUpService>();
provider.GetRequiredService<ILocationService>().Should().BeOfType<LocationService>();
provider.GetRequiredService<IDispatchService>().Should().BeOfType<DispatchService>();
provider.GetRequiredService<ISettingsService>().Should().BeOfType<SettingsService>();
}
[Fact]
public void ConventionDi_RegistersFollowUpLocationDispatchSettingsDataServices()
{
using var provider = BuildProvider();
provider.GetRequiredService<IFollowUpDataService>().Should().BeOfType<FollowUpDataService>();
provider.GetRequiredService<ILocationDataService>().Should().BeOfType<LocationDataService>();
provider.GetRequiredService<IDispatchDataService>().Should().BeOfType<DispatchDataService>();
provider.GetRequiredService<ISettingsDataService>().Should().BeOfType<SettingsDataService>();
}
} }

View file

@ -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<ISettingsService> service) =>
new(service.Object);
[Fact]
public async Task GetCategoryList_ReturnsPaginationEnvelopeWithServiceItems()
{
var service = new Mock<ISettingsService>();
service.Setup(s => s.GetCategoryListAsync(1, 10, It.IsAny<CancellationToken>()))
.ReturnsAsync((new List<SettingsCategoryDTO> { new() { Id = 1, Name = "A" } }, 1));
var result = await NewController(service).GetCategoryList(1, 10, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<Pagination_DTO>().Subject;
envelope.TotalCount.Should().Be(1);
envelope.TotalPages.Should().Be(1);
}
[Fact]
public async Task GetTemplates_ReturnsTypedServiceDtosInEnvelope()
{
var service = new Mock<ISettingsService>();
service.Setup(s => s.GetTemplatesAsync(1, 10, It.IsAny<CancellationToken>()))
.ReturnsAsync((new List<SettingsTemplateDTO> { new() { Id = 1, WorkerOrderTitle = "T" } }, 1));
var result = await NewController(service).GetTemplates(1, 10, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<Pagination_DTO>().Subject;
envelope.TotalCount.Should().Be(1);
}
[Fact]
public async Task AddCategory_WhenServiceSucceeds_ReturnsSaved()
{
var service = new Mock<ISettingsService>();
var result = await NewController(service).AddCategory(new Category_DTO { Name = "X" }, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var response = ok.Value.Should().BeOfType<DataResponse>().Subject;
response.Status.Should().Be("200");
response.Message.Should().Be("Successfully Saved Data");
service.Verify(s => s.AddCategoryAsync("X", It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task EditCategory_WhenMissing_ReturnsBadRequest()
{
var service = new Mock<ISettingsService>();
service.Setup(s => s.UpdateCategoryAsync(9, "X", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var result = await NewController(service).EditCategory(new EditCategory_DTO { Id = 9, Name = "X" }, CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
bad.Value.Should().BeOfType<Response>().Subject.Message.Should().Be("Category not found");
}
[Fact]
public async Task DeleteCategory_WhenMissing_ReturnsIdNotMatched()
{
var service = new Mock<ISettingsService>();
service.Setup(s => s.DeleteCategoryAsync(9, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var result = await NewController(service).DeleteCategory(9, CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
bad.Value.Should().BeOfType<Response>().Subject.Message.Should().Be("ID not matched!");
}
[Fact]
public async Task GetUsers_MapsServiceUsersToUserRoleViewModel()
{
var service = new Mock<ISettingsService>();
service.Setup(s => s.GetUsersAsync("", 1, 10, It.IsAny<CancellationToken>()))
.ReturnsAsync((new List<SettingsUserDTO>
{
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<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<Pagination_DTO>().Subject;
object data = envelope.Data!;
var rows = ((System.Collections.IEnumerable)data).Cast<UserRoleViewModel>().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<ISettingsService>();
service.Setup(s => s.GetRolesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<RoleDTO> { new() { Name = "Admin", Description = "Full Access" } });
var result = await NewController(service).GetRoles(CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var roles = ((System.Collections.IEnumerable)ok.Value!).Cast<Roles_DTO>().ToList();
roles.Should().ContainSingle(r => r.Name == "Admin" && r.Description == "Full Access");
}
}

View file

@ -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<ApplicationDbContext>()
.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<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager) NewIdentityContext()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddDbContext<ApplicationDbContext>(o => o.UseInMemoryDatabase(Guid.NewGuid().ToString()));
services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>();
var provider = services.BuildServiceProvider();
var scope = provider.CreateScope();
var ctx = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
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<SettingsTemplateDTO>();
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");
}
}

View file

@ -1,9 +1,7 @@
using Data.SeaHavenIndustries; using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces; using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.Controllers namespace Api.SeaHavenIndustries.Controllers
{ {
@ -14,12 +12,10 @@ namespace Api.SeaHavenIndustries.Controllers
public class DispatchController : Controller public class DispatchController : Controller
{ {
private readonly IDispatchService _dispatchService; private readonly IDispatchService _dispatchService;
private readonly ApplicationDbContext _db; // Keep for complex queries
public DispatchController(IDispatchService dispatchService, ApplicationDbContext db) public DispatchController(IDispatchService dispatchService)
{ {
_dispatchService = dispatchService; _dispatchService = dispatchService;
_db = db;
} }
[HttpGet] [HttpGet]
@ -32,317 +28,29 @@ namespace Api.SeaHavenIndustries.Controllers
[FromQuery] string? sortBy = "dispatchedAt", [FromQuery] string? sortBy = "dispatchedAt",
[FromQuery] string? sortDir = "desc", [FromQuery] string? sortDir = "desc",
[FromQuery] int page = 1, [FromQuery] int page = 1,
[FromQuery] int pageSize = 25) [FromQuery] int pageSize = 25,
CancellationToken cancellationToken = default)
{ {
var q = _db.Dispatches var result = await _dispatchService.GetDispatchListAsync(
.Include(d => d.Vendor) search, status, vendorId, dateFrom, dateTo, sortBy, sortDir, page, pageSize, cancellationToken);
.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();
return Ok(new DataResponse return Ok(new DataResponse
{ {
Status = "Success", 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}")] [HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id) public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
{ {
var dispatch = await _db.Dispatches var dispatch = await _dispatchService.GetDispatchDetailAsync(id, cancellationToken);
.Include(d => d.Vendor)
.Include(d => d.WorkOrder).ThenInclude(w => w!.Locations)
.FirstOrDefaultAsync(d => d.Id == id);
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" }); 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 return Ok(new DataResponse
{ {
Status = "Success", Status = "Success",
Data = new Data = dispatch
{
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
}
}); });
} }
} }

View file

@ -3,12 +3,9 @@ using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries; using Data.SeaHavenIndustries;
using FluentValidation; using FluentValidation;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs; using SeaHaven.Services.DTOs;
using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.Interfaces;
[ApiController] [ApiController]
[Authorize] [Authorize]
@ -17,16 +14,10 @@ using SeaHaven.DataServices.Interfaces;
public class FollowUpController : ControllerBase public class FollowUpController : ControllerBase
{ {
private readonly IFollowUpService _followUpService; private readonly IFollowUpService _followUpService;
private readonly IFollowUpDataService _followUpDataService; // Keep for direct entity operations
private readonly ApplicationDbContext _db; // Keep for User lookups
private readonly UserManager<ApplicationUser> _users;
public FollowUpController(IFollowUpService followUpService, IFollowUpDataService followUpDataService, ApplicationDbContext db, UserManager<ApplicationUser> users) public FollowUpController(IFollowUpService followUpService)
{ {
_followUpService = followUpService; _followUpService = followUpService;
_followUpDataService = followUpDataService;
_db = db;
_users = users;
} }
[HttpGet("GetFollowUpList")] [HttpGet("GetFollowUpList")]
@ -34,70 +25,31 @@ public class FollowUpController : ControllerBase
string? search = "", string? search = "",
string? status = "Open", string? status = "Open",
int page = 1, int page = 1,
int pageSize = 12) int pageSize = 12,
CancellationToken cancellationToken = default)
{ {
status = string.IsNullOrWhiteSpace(status) ? "Open" : status; var pagedResult = await _followUpService.GetFollowUpListAsync(page, pageSize, search, status, cancellationToken);
var pagedResult = await _followUpService.GetFollowUpsPagedAsync(page, pageSize, search, status);
var items = pagedResult.Items; var items = pagedResult.Items;
var rows = items.Select(x => new var data = items.Select(x => new
{ {
Id = x.Id, x.Id,
Reason = x.Reason, x.Reason,
ScheduleDate = x.ScheduleDate, x.ScheduleDate,
ScheduleStartTime = x.ScheduleStartTime, x.ScheduleStartTime,
EmployeeId = x.EmployeeId, x.EmployeeId,
WorkOrderId = x.WorkOrderId, x.WorkOrderId,
AccountId = x.AccountId, x.AccountId,
LocationId = x.LocationId, x.LocationId,
AccountName = x.AccountName ?? "", x.AccountName,
AssignedTo = x.EmployeeName ?? "", x.AssignedTo,
LocationName = x.LocationName ?? "", x.LocationName,
WorkOrderNumber = "", x.WorkOrderNumber,
InternalWONumber = "", x.InternalWONumber,
Status = x.Status, x.Status,
CreatedDate = x.CreatedDate, x.CreatedDate,
CreatedById = (string?)null x.CreatedById,
}).ToList(); x.CreatedByName
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)
}).ToList(); }).ToList();
return Ok(new Pagination_DTO return Ok(new Pagination_DTO
@ -111,26 +63,12 @@ public class FollowUpController : ControllerBase
} }
[HttpGet("GetById")] [HttpGet("GetById")]
public async Task<IActionResult> GetById(int id) public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
{ {
var model = await _followUpDataService.GetByIdWithDetailsAsync(id); var model = await _followUpService.GetFollowUpDetailAsync(id, cancellationToken);
if (model == null) return NotFound(); 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 return Ok(new
{ {
model.Id, model.Id,
@ -141,22 +79,20 @@ public class FollowUpController : ControllerBase
model.WorkOrderId, model.WorkOrderId,
model.AccountId, model.AccountId,
model.LocationId, model.LocationId,
AccountName = model.Account?.Name ?? "", model.AccountName,
AssignedTo = model.Employee != null model.AssignedTo,
? (model.Employee.FirstName + " " + model.Employee.LastName) model.LocationName,
: "", model.WorkOrderNumber,
LocationName = model.Location?.Name ?? "", model.InternalWONumber,
WorkOrderNumber = model.WorkOrder?.WorkerOrderNumber ?? "",
InternalWONumber = model.WorkOrder?.InternalWONumber ?? "",
model.Status, model.Status,
model.CreatedDate, model.CreatedDate,
CreatedById = model.createdby, model.CreatedById,
CreatedByName = createdByName model.CreatedByName
}); });
} }
[HttpPost("Create")] [HttpPost("Create")]
public async Task<IActionResult> Create(FollowUp_DTO dto) public async Task<IActionResult> Create(FollowUp_DTO dto, CancellationToken cancellationToken)
{ {
try try
{ {
@ -164,23 +100,9 @@ public class FollowUpController : ControllerBase
if (userId == null) if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" }); return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
var model = new FollowUps var id = await _followUpService.CreateOpenFollowUpAsync(MapToCreateRequest(dto), userId, cancellationToken);
{
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
};
await _followUpDataService.AddAsync(model); return Ok(new DataResponse { Status = "200", Message = "Follow Up created", Data = id });
return Ok(new DataResponse { Status = "200", Message = "Follow Up created", Data = model.Id });
} }
catch (ValidationException vex) catch (ValidationException vex)
{ {
@ -194,24 +116,11 @@ public class FollowUpController : ControllerBase
} }
[HttpPost("Update")] [HttpPost("Update")]
public async Task<IActionResult> Update(FollowUp_DTO dto) public async Task<IActionResult> Update(FollowUp_DTO dto, CancellationToken cancellationToken)
{ {
try try
{ {
var model = await _followUpDataService.GetByIdAsync(dto.Id); await _followUpService.UpdateFollowUpFieldsAsync(dto.Id, MapToUpdateRequest(dto), cancellationToken);
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);
return Ok(new DataResponse { Status = "200", Message = "Follow Up updated" }); return Ok(new DataResponse { Status = "200", Message = "Follow Up updated" });
} }
catch (ValidationException vex) catch (ValidationException vex)
@ -230,42 +139,32 @@ public class FollowUpController : ControllerBase
} }
[HttpPost("Complete")] [HttpPost("Complete")]
public async Task<IActionResult> Complete(int id) public async Task<IActionResult> Complete(int id, CancellationToken cancellationToken)
{ {
var model = await _followUpDataService.GetByIdAsync(id); var found = await _followUpService.SetFollowUpStatusAsync(id, "Completed", cancellationToken);
if (model == null) return NotFound(); if (!found) return NotFound();
model.Status = "Completed";
model.LastModificationTime = DateTime.UtcNow;
await _followUpDataService.UpdateAsync(model);
return Ok(); return Ok();
} }
[HttpPost("Cancel")] [HttpPost("Cancel")]
public async Task<IActionResult> Cancel(int id) public async Task<IActionResult> Cancel(int id, CancellationToken cancellationToken)
{ {
var model = await _followUpDataService.GetByIdAsync(id); var found = await _followUpService.SetFollowUpStatusAsync(id, "Cancelled", cancellationToken);
if (model == null) return NotFound(); if (!found) return NotFound();
model.Status = "Cancelled";
model.LastModificationTime = DateTime.UtcNow;
await _followUpDataService.UpdateAsync(model);
return Ok(); return Ok();
} }
[HttpPost("Delete")] [HttpPost("Delete")]
public async Task<IActionResult> Delete(int id) public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
{ {
try try
{ {
var model = await _followUpDataService.GetByIdAsync(id); var found = await _followUpService.RemoveFollowUpAsync(id, cancellationToken);
if (model == null) if (!found)
return NotFound(new Response { Status = "Error", Message = "Follow Up not 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" }); return Ok(new DataResponse { Status = "200", Message = "Follow Up deleted" });
} }
catch (KeyNotFoundException) catch (KeyNotFoundException)
@ -277,4 +176,33 @@ public class FollowUpController : ControllerBase
return StatusCode(500, new Response { Status = "Error", Message = ex.Message }); 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
};
}
} }

View file

@ -3,11 +3,8 @@ using Data.SeaHavenIndustries;
using FluentValidation; using FluentValidation;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs; using SeaHaven.Services.DTOs;
using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.Interfaces;
using System.Security.Claims;
namespace Api.SeaHavenIndustries.Controllers namespace Api.SeaHavenIndustries.Controllers
{ {
@ -18,18 +15,16 @@ namespace Api.SeaHavenIndustries.Controllers
public class LocationController : Controller public class LocationController : Controller
{ {
private readonly ILocationService _locationService; 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; _locationService = locationService;
_locationDataService = locationDataService;
} }
[HttpGet("GetLocationList")] [HttpGet("GetLocationList")]
public async Task<IActionResult> GetLocationList(string? search = "", int page = 1, int pageSize = 10) public async Task<IActionResult> 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 var data = pagedResult.Items.Select(l => new
{ {
@ -61,9 +56,9 @@ namespace Api.SeaHavenIndustries.Controllers
} }
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<IActionResult> GetLocationById(int id) public async Task<IActionResult> GetLocationById(int id, CancellationToken cancellationToken)
{ {
var location = await _locationDataService.GetByIdAsync(id); var location = await _locationService.GetLocationDetailAsync(id, cancellationToken);
if (location == null) if (location == null)
return NotFound(new Response { Status = "Error", Message = "Location not found" }); return NotFound(new Response { Status = "Error", Message = "Location not found" });
@ -89,25 +84,11 @@ namespace Api.SeaHavenIndustries.Controllers
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> AddLocation([FromBody] Location_DTO model) public async Task<IActionResult> AddLocation([FromBody] Location_DTO model, CancellationToken cancellationToken)
{ {
try try
{ {
var location = new Locations await _locationService.CreateLocationFromRequestAsync(MapToCreateRequest(model), cancellationToken);
{
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);
return Ok(new DataResponse { Message = "Location Created Successfully", Status = "200" }); return Ok(new DataResponse { Message = "Location Created Successfully", Status = "200" });
} }
catch (ValidationException vex) catch (ValidationException vex)
@ -122,26 +103,11 @@ namespace Api.SeaHavenIndustries.Controllers
} }
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<IActionResult> EditLocation(int id, [FromBody] EditLocation_DTO model) public async Task<IActionResult> EditLocation(int id, [FromBody] EditLocation_DTO model, CancellationToken cancellationToken)
{ {
try try
{ {
var exist = await _locationDataService.GetByIdAsync(id); await _locationService.UpdateLocationFromRequestAsync(id, MapToUpdateRequest(model), cancellationToken);
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);
return Ok(new DataResponse { Message = "Location Updated Successfully", Status = "200" }); return Ok(new DataResponse { Message = "Location Updated Successfully", Status = "200" });
} }
catch (ValidationException vex) catch (ValidationException vex)
@ -160,15 +126,14 @@ namespace Api.SeaHavenIndustries.Controllers
} }
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<IActionResult> DeleteLocation(int id) public async Task<IActionResult> DeleteLocation(int id, CancellationToken cancellationToken)
{ {
try try
{ {
var exist = await _locationDataService.GetByIdAsync(id); var found = await _locationService.DeleteLocationByIdAsync(id, cancellationToken);
if (exist == null) if (!found)
return NotFound(new Response { Status = "Error", Message = "Location not 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" }); return Ok(new DataResponse { Message = "Location Deleted Successfully", Status = "200" });
} }
catch (KeyNotFoundException) catch (KeyNotFoundException)
@ -182,9 +147,41 @@ namespace Api.SeaHavenIndustries.Controllers
} }
[HttpGet("DeleteLocation")] [HttpGet("DeleteLocation")]
public async Task<IActionResult> DeleteLocationByQuery(int id) public async Task<IActionResult> 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
};
} }
} }
} }

View file

@ -2,10 +2,8 @@
using Api.SeaHavenIndustries.Helper.ViewModel; using Api.SeaHavenIndustries.Helper.ViewModel;
using Data.SeaHavenIndustries; using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using SeaHaven.Services.Interfaces;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Api.SeaHavenIndustries.Controllers namespace Api.SeaHavenIndustries.Controllers
{ {
@ -14,24 +12,20 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("[controller]")] [Route("[controller]")]
public class SettingsController : Controller public class SettingsController : Controller
{ {
private readonly UserManager<ApplicationUser> _userManager; private readonly ISettingsService _settingsService;
private readonly ApplicationDbContext _db;
public SettingsController(UserManager<ApplicationUser> userManager, ApplicationDbContext db) public SettingsController(ISettingsService settingsService)
{ {
_userManager = userManager; _settingsService = settingsService;
_db = db;
} }
[HttpPost] [HttpPost]
[Route("AddCategory")] [Route("AddCategory")]
public async Task<IActionResult> AddCategory(Category_DTO model) public async Task<IActionResult> AddCategory(Category_DTO model, CancellationToken cancellationToken)
{ {
try try
{ {
Category category = new Category(); await _settingsService.AddCategoryAsync(model.Name, cancellationToken);
category.Name = model.Name;
_db.Categories.Add(category);
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Successfully Saved Data", Status = "200" }); return Ok(new DataResponse { Message = "Successfully Saved Data", Status = "200" });
} }
catch (Exception ex) catch (Exception ex)
@ -42,16 +36,13 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpPost] [HttpPost]
[Route("EditCategory")] [Route("EditCategory")]
public async Task<IActionResult> EditCategory(EditCategory_DTO model) public async Task<IActionResult> EditCategory(EditCategory_DTO model, CancellationToken cancellationToken)
{ {
try try
{ {
var exist = _db.Categories.Where(w => w.Id == model.Id).FirstOrDefault(); var found = await _settingsService.UpdateCategoryAsync(model.Id, model.Name, cancellationToken);
if (exist == null) if (!found)
return BadRequest(new Response { Status = "Error", Message = "Category not 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" }); return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
} }
catch (Exception ex) catch (Exception ex)
@ -62,30 +53,13 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpGet] [HttpGet]
[Route("DeleteCategory")] [Route("DeleteCategory")]
public async Task<IActionResult> DeleteCategory(int id) public async Task<IActionResult> DeleteCategory(int id, CancellationToken cancellationToken)
{ {
try try
{ {
var exist = _db.Categories.Where(w => w.Id == id).FirstOrDefault(); var found = await _settingsService.DeleteCategoryAsync(id, cancellationToken);
if (exist != null) if (!found)
{
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
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" }); return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Deleted Successfully", Status = "200" }); return Ok(new DataResponse { Message = "Deleted Successfully", Status = "200" });
} }
catch (Exception ex) catch (Exception ex)
@ -94,27 +68,20 @@ namespace Api.SeaHavenIndustries.Controllers
} }
} }
[HttpGet] [HttpGet]
[Route("GetCategoryList")] [Route("GetCategoryList")]
public IActionResult GetCategoryList(int page = 1, int pageSize = 10) public async Task<IActionResult> GetCategoryList(int page = 1, int pageSize = 10, CancellationToken cancellationToken = default)
{ {
var totalCount = _db.Categories.Count(); var (items, total) = await _settingsService.GetCategoryListAsync(page, pageSize, cancellationToken);
var data = _db.Categories.Select(s => new
{
s.Id,
s.Name
}).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize)
.Take(pageSize).ToList();
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); var totalPages = (int)Math.Ceiling(total / (double)pageSize);
var viewModel = new Pagination_DTO var viewModel = new Pagination_DTO
{ {
Data = data, Data = items,
PageNumber = page, PageNumber = page,
PageSize = pageSize, PageSize = pageSize,
TotalCount = totalCount, TotalCount = total,
TotalPages = totalPages TotalPages = totalPages
}; };
return Ok(viewModel); return Ok(viewModel);
@ -122,24 +89,20 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpGet] [HttpGet]
[Route("GetTemplates")] [Route("GetTemplates")]
public IActionResult GetTemplates(int page = 1, int pageSize = 10) public async Task<IActionResult> GetTemplates(int page = 1, int pageSize = 10, CancellationToken cancellationToken = default)
{ {
try try
{ {
var totalCount = _db.workOrders.Where(w=> w.istemplate == true).Count(); var (items, total) = await _settingsService.GetTemplatesAsync(page, pageSize, cancellationToken);
var data = _db.workOrders.Where(w => w.istemplate == true).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize)
.Take(pageSize).ToList();
// Calculate the number of pages var totalPages = (int)Math.Ceiling(total / (double)pageSize);
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
// Create a view model containing the paged items and pagination metadata
var viewModel = new Pagination_DTO var viewModel = new Pagination_DTO
{ {
Data = data, Data = items,
PageNumber = page, PageNumber = page,
PageSize = pageSize, PageSize = pageSize,
TotalCount = totalCount, TotalCount = total,
TotalPages = totalPages TotalPages = totalPages
}; };
return Ok(viewModel); return Ok(viewModel);
@ -152,42 +115,30 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpGet] [HttpGet]
[Route("GetUsers")] [Route("GetUsers")]
public async Task<IActionResult> GetUsers(string? search = "", int page = 1, int pageSize = 10) public async Task<IActionResult> GetUsers(string? search = "", int page = 1, int pageSize = 10, CancellationToken cancellationToken = default)
{ {
try try
{ {
var totalCount = _db.Users.Count(); var (users, total) = await _settingsService.GetUsersAsync(search, page, pageSize, cancellationToken);
var allUsers = _db.Users.Where(w => (w.FirstName ?? "").ToLower().Contains((search ?? "").ToLower())).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize)
.Take(pageSize).ToList();
// Calculate the number of pages var filteredUsers = users.Select(u => new UserRoleViewModel
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); {
RoleName = u.RoleName,
Email = u.Email,
UserName = u.UserName,
Date = u.Date,
Status = u.Status,
Id = u.Id
}).ToList();
var filteredUsers = new List<UserRoleViewModel>(); var totalPages = (int)Math.Ceiling(total / (double)pageSize);
foreach (var user in allUsers)
{
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
var viewModel = new Pagination_DTO var viewModel = new Pagination_DTO
{ {
Data = filteredUsers, Data = filteredUsers,
PageNumber = page, PageNumber = page,
PageSize = pageSize, PageSize = pageSize,
TotalCount = totalCount, TotalCount = total,
TotalPages = totalPages TotalPages = totalPages
}; };
return Ok(viewModel); return Ok(viewModel);
@ -200,35 +151,13 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpGet] [HttpGet]
[Route("GetRoles")] [Route("GetRoles")]
public async Task<IActionResult> GetRoles() public async Task<IActionResult> GetRoles(CancellationToken cancellationToken)
{ {
try try
{ {
var data = _db.Roles.ToList(); var roles = await _settingsService.GetRolesAsync(cancellationToken);
List<Roles_DTO> roles = new List<Roles_DTO>(); var result = roles.Select(r => new Roles_DTO { Name = r.Name, Description = r.Description }).ToList();
foreach (var role in data) return Ok(result);
{
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);
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -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<DispatchListItemDTO> Items { get; set; } = Array.Empty<DispatchListItemDTO>();
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<DispatchLinkedWorkOrderDTO> WorkOrders { get; set; } = Array.Empty<DispatchLinkedWorkOrderDTO>();
public IReadOnlyList<DispatchChecklistDTO> Checklist { get; set; } = Array.Empty<DispatchChecklistDTO>();
public IReadOnlyList<DispatchSignoffDTO> Signoffs { get; set; } = Array.Empty<DispatchSignoffDTO>();
public IReadOnlyList<DispatchCommentDTO> Comments { get; set; } = Array.Empty<DispatchCommentDTO>();
public IReadOnlyList<DispatchUpliftRequestDTO> UpliftRequests { get; set; } = Array.Empty<DispatchUpliftRequestDTO>();
public IReadOnlyList<DispatchAuditLogDTO> AuditLog { get; set; } = Array.Empty<DispatchAuditLogDTO>();
}
}

View file

@ -0,0 +1,8 @@
namespace SeaHaven.DataServices.Dto
{
public class SettingsCategoryItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
}
}

View file

@ -1,5 +1,6 @@
using Data.SeaHavenIndustries; using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Dto;
using SeaHaven.DataServices.Interfaces; using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation namespace SeaHaven.DataServices.Implementation
@ -216,5 +217,346 @@ namespace SeaHaven.DataServices.Implementation
await _context.DispatchWorkOrders.AddAsync(dispatchWorkOrder); await _context.DispatchWorkOrders.AddAsync(dispatchWorkOrder);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
} }
public async Task<DispatchListResult> 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<DispatchDetailDTO?> 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<DispatchLinkedWorkOrderDTO>
{
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
};
}
} }
} }

View file

@ -120,5 +120,138 @@ namespace SeaHaven.DataServices.Implementation
{ {
return await _context.FollowUps.CountAsync(); return await _context.FollowUps.CountAsync();
} }
public async Task<List<FollowUps>> 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<int> 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<FollowUps?> 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<FollowUps?> GetByIdForUpdateAsync(int id, CancellationToken cancellationToken)
{
return await _context.FollowUps.FindAsync(new object[] { id }, cancellationToken);
}
public async Task<string> 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<FollowUps> 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<bool> 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<bool> 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;
}
} }
} }

View file

@ -95,6 +95,69 @@ namespace SeaHaven.DataServices.Implementation
return await _context.Locations.CountAsync(); return await _context.Locations.CountAsync();
} }
public async Task<(List<Locations> 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<Locations?> GetDetailByIdAsync(int id, CancellationToken cancellationToken)
{
return await _context.Locations
.AsNoTracking()
.FirstOrDefaultAsync(l => l.Id == id, cancellationToken);
}
public async Task<Locations?> GetByIdForUpdateAsync(int id, CancellationToken cancellationToken)
{
return await _context.Locations.FindAsync(new object[] { id }, cancellationToken);
}
public async Task<Locations> 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<bool> 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<object> Items, int TotalCount)> GetAddressbookPagedAsync( public async Task<(IEnumerable<object> Items, int TotalCount)> GetAddressbookPagedAsync(
int page, int page,
int pageSize, int pageSize,

View file

@ -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<bool> 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<bool> 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<SettingsCategoryItemDTO> 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<WorkOrder> 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<ApplicationUser> 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<IReadOnlyList<string>> GetRoleNamesAsync(CancellationToken cancellationToken)
{
var roles = await _context.Roles.AsNoTracking().ToListAsync(cancellationToken);
return roles.Select(r => r.Name!).ToList();
}
}
}

View file

@ -1,4 +1,5 @@
using Data.SeaHavenIndustries; using Data.SeaHavenIndustries;
using SeaHaven.DataServices.Dto;
namespace SeaHaven.DataServices.Interfaces namespace SeaHaven.DataServices.Interfaces
{ {
@ -28,5 +29,8 @@ namespace SeaHaven.DataServices.Interfaces
Task<bool> HasSignoffTypeAsync(int dispatchId, string signoffType); Task<bool> HasSignoffTypeAsync(int dispatchId, string signoffType);
Task<IEnumerable<DispatchWorkOrder>> GetDispatchWorkOrdersAsync(int dispatchId); Task<IEnumerable<DispatchWorkOrder>> GetDispatchWorkOrdersAsync(int dispatchId);
Task AddDispatchWorkOrderAsync(DispatchWorkOrder dispatchWorkOrder); Task AddDispatchWorkOrderAsync(DispatchWorkOrder dispatchWorkOrder);
Task<DispatchListResult> GetListAsync(DispatchListQuery query, CancellationToken cancellationToken);
Task<DispatchDetailDTO?> GetDetailAsync(int id, CancellationToken cancellationToken);
} }
} }

View file

@ -15,5 +15,15 @@ namespace SeaHaven.DataServices.Interfaces
Task DeleteAsync(int id); Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id); Task<bool> ExistsAsync(int id);
Task<int> CountAsync(); Task<int> CountAsync();
Task<List<FollowUps>> GetListPagedAsync(int page, int pageSize, string? search, string? status, CancellationToken cancellationToken);
Task<int> GetListTotalAsync(string? search, string? status, CancellationToken cancellationToken);
Task<FollowUps?> GetDetailByIdAsync(int id, CancellationToken cancellationToken);
Task<FollowUps?> GetByIdForUpdateAsync(int id, CancellationToken cancellationToken);
Task<string> GetUserNameAsync(string? userId, CancellationToken cancellationToken);
Task<FollowUps> AddAsync(FollowUps followUp, CancellationToken cancellationToken);
Task UpdateAsync(FollowUps followUp, CancellationToken cancellationToken);
Task<bool> SetStatusAsync(int id, string status, CancellationToken cancellationToken);
Task<bool> RemoveByIdAsync(int id, CancellationToken cancellationToken);
} }
} }

View file

@ -20,5 +20,12 @@ namespace SeaHaven.DataServices.Interfaces
Task DeleteAsync(int id); Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id); Task<bool> ExistsAsync(int id);
Task<int> CountAsync(); Task<int> CountAsync();
Task<(List<Locations> Items, int TotalCount)> GetListPagedAsync(int page, int pageSize, string? search, CancellationToken cancellationToken);
Task<Locations?> GetDetailByIdAsync(int id, CancellationToken cancellationToken);
Task<Locations?> GetByIdForUpdateAsync(int id, CancellationToken cancellationToken);
Task<Locations> AddAsync(Locations location, CancellationToken cancellationToken);
Task UpdateAsync(Locations location, CancellationToken cancellationToken);
Task<bool> DeleteByIdAsync(int id, CancellationToken cancellationToken);
} }
} }

View file

@ -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<bool> UpdateCategoryAsync(int id, string name, CancellationToken cancellationToken);
Task<bool> DeleteCategoryCascadeAsync(int id, CancellationToken cancellationToken);
Task<(IReadOnlyList<SettingsCategoryItemDTO> Items, int Total)> GetCategoryListPagedAsync(int page, int pageSize, CancellationToken cancellationToken);
Task<(IReadOnlyList<WorkOrder> Items, int Total)> GetTemplatesPagedAsync(int page, int pageSize, CancellationToken cancellationToken);
Task<(IReadOnlyList<ApplicationUser> Users, int Total)> GetUsersPagedAsync(string? search, int page, int pageSize, CancellationToken cancellationToken);
Task<IReadOnlyList<string>> GetRoleNamesAsync(CancellationToken cancellationToken);
}
}

View file

@ -27,4 +27,158 @@ namespace SeaHaven.Services.DTOs
public DateTime? CompletedDate { get; set; } public DateTime? CompletedDate { get; set; }
public string? Status { 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<DispatchListEntryDTO> Items { get; set; } = Array.Empty<DispatchListEntryDTO>();
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<DispatchDetailLinkedWorkOrderDTO> WorkOrders { get; set; } = Array.Empty<DispatchDetailLinkedWorkOrderDTO>();
public IReadOnlyList<DispatchDetailChecklistDTO> Checklist { get; set; } = Array.Empty<DispatchDetailChecklistDTO>();
public IReadOnlyList<DispatchDetailSignoffDTO> Signoffs { get; set; } = Array.Empty<DispatchDetailSignoffDTO>();
public IReadOnlyList<DispatchDetailCommentDTO> Comments { get; set; } = Array.Empty<DispatchDetailCommentDTO>();
public IReadOnlyList<DispatchDetailUpliftDTO> UpliftRequests { get; set; } = Array.Empty<DispatchDetailUpliftDTO>();
public IReadOnlyList<DispatchDetailAuditLogDTO> AuditLog { get; set; } = Array.Empty<DispatchDetailAuditLogDTO>();
}
} }

View file

@ -40,4 +40,69 @@ namespace SeaHaven.Services.DTOs
public int? AccountId { get; set; } public int? AccountId { get; set; }
public int? LocationId { 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; } = "";
}
} }

View file

@ -36,4 +36,30 @@ namespace SeaHaven.Services.DTOs
public string? Zipcode { get; set; } public string? Zipcode { get; set; }
public int? AccountId { 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; }
}
} }

View file

@ -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; }
}
}

View file

@ -1,5 +1,6 @@
using Data.SeaHavenIndustries; using Data.SeaHavenIndustries;
using FluentValidation; using FluentValidation;
using SeaHaven.DataServices.Dto;
using SeaHaven.DataServices.Interfaces; using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs; using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces; using SeaHaven.Services.Interfaces;
@ -141,6 +142,184 @@ namespace SeaHaven.Services.Implementation
return await _dispatchDataService.CountAsync(); return await _dispatchDataService.CountAsync();
} }
public async Task<DispatchListResultDTO> 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<DispatchDetailResultDTO?> 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) private DispatchDTO MapToDTO(Dispatch dispatch)
{ {
return new DispatchDTO return new DispatchDTO

View file

@ -148,6 +148,131 @@ namespace SeaHaven.Services.Implementation
return await _followUpDataService.CountAsync(); return await _followUpDataService.CountAsync();
} }
public async Task<PagedResult<FollowUpListRowDTO>> 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<FollowUpListRowDTO>
{
Items = items.Select(MapToListRow).ToList(),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<FollowUpDetailDTO?> 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<int> 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<bool> SetFollowUpStatusAsync(int id, string status, CancellationToken cancellationToken)
{
return _followUpDataService.SetStatusAsync(id, status, cancellationToken);
}
public Task<bool> 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) private FollowUpDTO MapToDTO(FollowUps followUp)
{ {
return new FollowUpDTO return new FollowUpDTO

View file

@ -139,6 +139,70 @@ namespace SeaHaven.Services.Implementation
return await _locationDataService.CountAsync(); return await _locationDataService.CountAsync();
} }
public async Task<PagedResult<LocationDTO>> GetLocationListPagedAsync(
int page,
int pageSize,
string? search,
CancellationToken cancellationToken)
{
var (items, totalCount) = await _locationDataService.GetListPagedAsync(page, pageSize, search, cancellationToken);
return new PagedResult<LocationDTO>
{
Items = items.Select(MapToDTO).ToList(),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<LocationDTO?> 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<bool> DeleteLocationByIdAsync(int id, CancellationToken cancellationToken)
{
return _locationDataService.DeleteByIdAsync(id, cancellationToken);
}
// Private helper methods // Private helper methods
private LocationDTO MapToDTO(Locations location) private LocationDTO MapToDTO(Locations location)
{ {

View file

@ -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<ApplicationUser> _userManager;
public SettingsService(ISettingsDataService dataService, UserManager<ApplicationUser> userManager)
{
_dataService = dataService;
_userManager = userManager;
}
public async Task AddCategoryAsync(string? name, CancellationToken cancellationToken)
{
await _dataService.AddCategoryAsync(name ?? "", cancellationToken);
}
public Task<bool> UpdateCategoryAsync(int? id, string? name, CancellationToken cancellationToken)
{
return _dataService.UpdateCategoryAsync(id ?? 0, name ?? "", cancellationToken);
}
public Task<bool> DeleteCategoryAsync(int id, CancellationToken cancellationToken)
{
return _dataService.DeleteCategoryCascadeAsync(id, cancellationToken);
}
public async Task<(IReadOnlyList<SettingsCategoryDTO> 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<SettingsTemplateDTO> 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<SettingsUserDTO> 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<SettingsUserDTO>();
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<IReadOnlyList<RoleDTO>> GetRolesAsync(CancellationToken cancellationToken)
{
var names = await _dataService.GetRoleNamesAsync(cancellationToken);
var roles = new List<RoleDTO>();
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
};
}
}
}

View file

@ -15,5 +15,8 @@ namespace SeaHaven.Services.Interfaces
Task<bool> DispatchExistsAsync(int id); Task<bool> DispatchExistsAsync(int id);
Task<string> GenerateNextDispatchNumberAsync(); Task<string> GenerateNextDispatchNumberAsync();
Task<int> GetTotalDispatchCountAsync(); Task<int> GetTotalDispatchCountAsync();
Task<DispatchListResultDTO> GetDispatchListAsync(string? search, string? status, int? vendorId, DateTime? dateFrom, DateTime? dateTo, string? sortBy, string? sortDir, int page, int pageSize, CancellationToken cancellationToken);
Task<DispatchDetailResultDTO?> GetDispatchDetailAsync(int id, CancellationToken cancellationToken);
} }
} }

View file

@ -14,5 +14,12 @@ namespace SeaHaven.Services.Interfaces
Task DeleteFollowUpAsync(int id, string userId); Task DeleteFollowUpAsync(int id, string userId);
Task<bool> FollowUpExistsAsync(int id); Task<bool> FollowUpExistsAsync(int id);
Task<int> GetTotalFollowUpCountAsync(); Task<int> GetTotalFollowUpCountAsync();
Task<PagedResult<FollowUpListRowDTO>> GetFollowUpListAsync(int page, int pageSize, string? search, string? status, CancellationToken cancellationToken);
Task<FollowUpDetailDTO?> GetFollowUpDetailAsync(int id, CancellationToken cancellationToken);
Task<int> CreateOpenFollowUpAsync(FollowUpCreateRequestDTO dto, string userId, CancellationToken cancellationToken);
Task UpdateFollowUpFieldsAsync(int id, FollowUpUpdateRequestDTO dto, CancellationToken cancellationToken);
Task<bool> SetFollowUpStatusAsync(int id, string status, CancellationToken cancellationToken);
Task<bool> RemoveFollowUpAsync(int id, CancellationToken cancellationToken);
} }
} }

View file

@ -14,5 +14,11 @@ namespace SeaHaven.Services.Interfaces
Task DeleteLocationAsync(int id, string userId); Task DeleteLocationAsync(int id, string userId);
Task<bool> LocationExistsAsync(int id); Task<bool> LocationExistsAsync(int id);
Task<int> GetTotalLocationCountAsync(); Task<int> GetTotalLocationCountAsync();
Task<PagedResult<LocationDTO>> GetLocationListPagedAsync(int page, int pageSize, string? search, CancellationToken cancellationToken);
Task<LocationDTO?> GetLocationDetailAsync(int id, CancellationToken cancellationToken);
Task CreateLocationFromRequestAsync(LocationCreateRequestDTO request, CancellationToken cancellationToken);
Task UpdateLocationFromRequestAsync(int id, LocationUpdateRequestDTO request, CancellationToken cancellationToken);
Task<bool> DeleteLocationByIdAsync(int id, CancellationToken cancellationToken);
} }
} }

View file

@ -0,0 +1,15 @@
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Interfaces
{
public interface ISettingsService
{
Task AddCategoryAsync(string? name, CancellationToken cancellationToken);
Task<bool> UpdateCategoryAsync(int? id, string? name, CancellationToken cancellationToken);
Task<bool> DeleteCategoryAsync(int id, CancellationToken cancellationToken);
Task<(IReadOnlyList<SettingsCategoryDTO> Items, int Total)> GetCategoryListAsync(int page, int pageSize, CancellationToken cancellationToken);
Task<(IReadOnlyList<SettingsTemplateDTO> Items, int Total)> GetTemplatesAsync(int page, int pageSize, CancellationToken cancellationToken);
Task<(IReadOnlyList<SettingsUserDTO> Users, int Total)> GetUsersAsync(string? search, int page, int pageSize, CancellationToken cancellationToken);
Task<IReadOnlyList<RoleDTO>> GetRolesAsync(CancellationToken cancellationToken);
}
}