mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-06 12:22:08 +00:00
feat(locations): add sites options endpoint and vendor contactName (#20)
* fix(migrations): make Phase6 completion slide-over migration idempotent Use IF EXISTS / IF COL_LENGTH guards and explicit EF migration metadata so partial environments can apply Phase 6 safely. * feat(locations): add sites options endpoint and vendor contactName Expose GET /api/locations/sites for the WO create wizard and include vendor contactName on dropdown responses. * fix(work-orders): make stacked tip build for CI quality gates --------- Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com>
This commit is contained in:
parent
af1c0d19b4
commit
8c4102b1cf
8 changed files with 163 additions and 0 deletions
109
Api.SeaHavenIndustries.Tests/LocationControllerSitesTests.cs
Normal file
109
Api.SeaHavenIndustries.Tests/LocationControllerSitesTests.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
using Api.SeaHavenIndustries.Controllers;
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moq;
|
||||||
|
using SeaHaven.DataServices.Implementation;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Implementation;
|
||||||
|
using SeaHaven.Services.Validation;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Tests;
|
||||||
|
|
||||||
|
public class LocationControllerSitesTests
|
||||||
|
{
|
||||||
|
private static ApplicationDbContext NewContext()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||||
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
return new ApplicationDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocationController NewController(ApplicationDbContext ctx)
|
||||||
|
{
|
||||||
|
var dataService = new LocationDataService(ctx);
|
||||||
|
var service = new LocationService(
|
||||||
|
dataService,
|
||||||
|
Mock.Of<ICreateLocationValidation>(),
|
||||||
|
Mock.Of<IUpdateLocationValidation>());
|
||||||
|
|
||||||
|
var controller = new LocationController(service, dataService)
|
||||||
|
{
|
||||||
|
ControllerContext = new ControllerContext
|
||||||
|
{
|
||||||
|
HttpContext = new DefaultHttpContext()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SeedSitesAsync(ApplicationDbContext ctx)
|
||||||
|
{
|
||||||
|
ctx.Locations.AddRange(
|
||||||
|
new Locations { Id = 1, Name = "STL8", City = "St. Louis", State = "MO" },
|
||||||
|
new Locations { Id = 2, Name = "BK5", City = "Dallas", State = "TX" },
|
||||||
|
new Locations { Id = 3, Name = "IND9", City = "Indianapolis", State = "IN" },
|
||||||
|
new Locations { Id = 4, Name = "DFW8", City = "Dallas", State = "TX" });
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetSites_ReturnsIdCodeCityState_OrderedByCode()
|
||||||
|
{
|
||||||
|
await using var ctx = NewContext();
|
||||||
|
await SeedSitesAsync(ctx);
|
||||||
|
var controller = NewController(ctx);
|
||||||
|
|
||||||
|
var result = await controller.GetSites();
|
||||||
|
|
||||||
|
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||||
|
var data = ok.Value.Should().BeAssignableTo<IReadOnlyList<SiteOptionDTO>>().Subject;
|
||||||
|
|
||||||
|
data.Should().HaveCount(4);
|
||||||
|
data.Select(s => s.Code).Should().ContainInOrder("BK5", "DFW8", "IND9", "STL8");
|
||||||
|
data[0].Should().BeEquivalentTo(new SiteOptionDTO
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
Code = "BK5",
|
||||||
|
City = "Dallas",
|
||||||
|
State = "TX"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetSites_FiltersByCode()
|
||||||
|
{
|
||||||
|
await using var ctx = NewContext();
|
||||||
|
await SeedSitesAsync(ctx);
|
||||||
|
var controller = NewController(ctx);
|
||||||
|
|
||||||
|
var result = await controller.GetSites("bk5");
|
||||||
|
|
||||||
|
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||||
|
var data = ok.Value.Should().BeAssignableTo<IReadOnlyList<SiteOptionDTO>>().Subject;
|
||||||
|
|
||||||
|
data.Should().ContainSingle();
|
||||||
|
data[0].Code.Should().Be("BK5");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetSites_FiltersByCity()
|
||||||
|
{
|
||||||
|
await using var ctx = NewContext();
|
||||||
|
await SeedSitesAsync(ctx);
|
||||||
|
var controller = NewController(ctx);
|
||||||
|
|
||||||
|
var result = await controller.GetSites("Dallas");
|
||||||
|
|
||||||
|
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||||
|
var data = ok.Value.Should().BeAssignableTo<IReadOnlyList<SiteOptionDTO>>().Subject;
|
||||||
|
|
||||||
|
data.Should().HaveCount(2);
|
||||||
|
data.Select(s => s.Code).Should().BeEquivalentTo("BK5", "DFW8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,13 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
_locationDataService = locationDataService;
|
_locationDataService = locationDataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("sites")]
|
||||||
|
public async Task<IActionResult> GetSites(string? search = null)
|
||||||
|
{
|
||||||
|
var data = await _locationService.GetSiteOptionsAsync(search);
|
||||||
|
return Ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
[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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
{
|
{
|
||||||
v.Id,
|
v.Id,
|
||||||
v.CompanyName,
|
v.CompanyName,
|
||||||
|
v.ContactName,
|
||||||
v.TradeSpecialties,
|
v.TradeSpecialties,
|
||||||
v.Address,
|
v.Address,
|
||||||
v.City,
|
v.City,
|
||||||
|
|
@ -191,6 +192,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
{
|
{
|
||||||
v.Id,
|
v.Id,
|
||||||
v.CompanyName,
|
v.CompanyName,
|
||||||
|
v.ContactName,
|
||||||
v.TradeSpecialties,
|
v.TradeSpecialties,
|
||||||
address = string.Join(", ", addr),
|
address = string.Join(", ", addr),
|
||||||
distanceMiles = distance.HasValue ? Math.Round(distance.Value, 1) : (double?)null
|
distanceMiles = distance.HasValue ? Math.Round(distance.Value, 1) : (double?)null
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,24 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
return (items, totalCount);
|
return (items, totalCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SiteOptionRow>> GetSiteOptionsAsync(string? search = null)
|
||||||
|
{
|
||||||
|
var query = _context.Locations.AsNoTracking();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
|
{
|
||||||
|
var s = search.ToLower();
|
||||||
|
query = query.Where(l =>
|
||||||
|
(l.Name ?? "").ToLower().Contains(s) ||
|
||||||
|
(l.City ?? "").ToLower().Contains(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query
|
||||||
|
.OrderBy(l => l.Name)
|
||||||
|
.Select(l => new SiteOptionRow(l.Id, l.Name, l.City, l.State))
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Locations> AddAsync(Locations location)
|
public async Task<Locations> AddAsync(Locations location)
|
||||||
{
|
{
|
||||||
location.CreatedDate = DateTime.Now;
|
location.CreatedDate = DateTime.Now;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ using Data.SeaHavenIndustries;
|
||||||
|
|
||||||
namespace SeaHaven.DataServices.Interfaces
|
namespace SeaHaven.DataServices.Interfaces
|
||||||
{
|
{
|
||||||
|
public record SiteOptionRow(int Id, string? Code, string? City, string? State);
|
||||||
|
|
||||||
public interface ILocationDataService
|
public interface ILocationDataService
|
||||||
{
|
{
|
||||||
Task<Locations?> GetByIdAsync(int id);
|
Task<Locations?> GetByIdAsync(int id);
|
||||||
|
|
@ -15,6 +17,8 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
// For addressbook endpoint - returns simplified DTO
|
// For addressbook endpoint - returns simplified DTO
|
||||||
Task<(IEnumerable<object> Items, int TotalCount)> GetAddressbookPagedAsync(int page, int pageSize, string? search = null);
|
Task<(IEnumerable<object> Items, int TotalCount)> GetAddressbookPagedAsync(int page, int pageSize, string? search = null);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<SiteOptionRow>> GetSiteOptionsAsync(string? search = null);
|
||||||
|
|
||||||
Task<Locations> AddAsync(Locations location);
|
Task<Locations> AddAsync(Locations location);
|
||||||
Task UpdateAsync(Locations location);
|
Task UpdateAsync(Locations location);
|
||||||
Task DeleteAsync(int id);
|
Task DeleteAsync(int id);
|
||||||
|
|
|
||||||
|
|
@ -36,4 +36,12 @@ 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 SiteOptionDTO
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? Code { get; set; }
|
||||||
|
public string? City { get; set; }
|
||||||
|
public string? State { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,20 @@ namespace SeaHaven.Services.Implementation
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SiteOptionDTO>> GetSiteOptionsAsync(string? search = null)
|
||||||
|
{
|
||||||
|
var rows = await _locationDataService.GetSiteOptionsAsync(search);
|
||||||
|
return rows
|
||||||
|
.Select(r => new SiteOptionDTO
|
||||||
|
{
|
||||||
|
Id = r.Id,
|
||||||
|
Code = r.Code,
|
||||||
|
City = r.City,
|
||||||
|
State = r.State
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<LocationDTO> CreateLocationAsync(CreateLocationDTO dto, string userId)
|
public async Task<LocationDTO> CreateLocationAsync(CreateLocationDTO dto, string userId)
|
||||||
{
|
{
|
||||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ namespace SeaHaven.Services.Interfaces
|
||||||
Task<IEnumerable<LocationDTO>> GetAllLocationsAsync();
|
Task<IEnumerable<LocationDTO>> GetAllLocationsAsync();
|
||||||
Task<IEnumerable<LocationDTO>> GetLocationsByAccountIdAsync(int accountId);
|
Task<IEnumerable<LocationDTO>> GetLocationsByAccountIdAsync(int accountId);
|
||||||
Task<PagedResult<LocationDTO>> GetLocationsPagedAsync(int page, int pageSize, string? search = null);
|
Task<PagedResult<LocationDTO>> GetLocationsPagedAsync(int page, int pageSize, string? search = null);
|
||||||
|
Task<IReadOnlyList<SiteOptionDTO>> GetSiteOptionsAsync(string? search = null);
|
||||||
Task<LocationDTO> CreateLocationAsync(CreateLocationDTO dto, string userId);
|
Task<LocationDTO> CreateLocationAsync(CreateLocationDTO dto, string userId);
|
||||||
Task<LocationDTO> UpdateLocationAsync(int id, UpdateLocationDTO dto, string userId);
|
Task<LocationDTO> UpdateLocationAsync(int id, UpdateLocationDTO dto, string userId);
|
||||||
Task DeleteLocationAsync(int id, string userId);
|
Task DeleteLocationAsync(int id, string userId);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue