mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 16:19:07 +00:00
feat(vendors): add directory filters and details API
This commit is contained in:
parent
b2aeb35a55
commit
1a5587126e
13 changed files with 781 additions and 25 deletions
113
Api.SeaHavenIndustries.Tests/VendorDataServiceTests.cs
Normal file
113
Api.SeaHavenIndustries.Tests/VendorDataServiceTests.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Implementation;
|
||||
using Xunit;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Tests;
|
||||
|
||||
public class VendorDataServiceTests
|
||||
{
|
||||
private static ApplicationDbContext NewContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new ApplicationDbContext(options);
|
||||
}
|
||||
|
||||
private static Vendor VendorWithJobs(
|
||||
string company,
|
||||
string city,
|
||||
string state,
|
||||
string trades,
|
||||
int jobCount,
|
||||
bool isActive = true)
|
||||
{
|
||||
return new Vendor
|
||||
{
|
||||
CompanyName = company,
|
||||
ContactName = $"{company} Technician",
|
||||
City = city,
|
||||
State = state,
|
||||
TradeSpecialties = trades,
|
||||
IsActive = isActive,
|
||||
IsDeleted = false,
|
||||
Dispatches = Enumerable.Range(1, jobCount)
|
||||
.Select(workOrderId => new Dispatch { WorkOrderId = workOrderId })
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task SeedBoundaryVendors(ApplicationDbContext context)
|
||||
{
|
||||
context.Vendors.AddRange(
|
||||
VendorWithJobs("Under Co", "Chicago", "IL", "Plumbing, HVAC", 49),
|
||||
VendorWithJobs("Fifty Co", "Chicago", "IL", "Electrical", 50),
|
||||
VendorWithJobs("Hundred Co", "Dallas", "TX", "Plumbing", 100),
|
||||
VendorWithJobs("One Fifty Co", "Dallas", "TX", "Roofing", 150));
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("under-50", "Under Co")]
|
||||
[InlineData("50-99", "Fifty Co")]
|
||||
[InlineData("100-149", "Hundred Co")]
|
||||
[InlineData("150-plus", "One Fifty Co")]
|
||||
public async Task GetPagedAsync_AppliesExactJobBucketBoundaries(
|
||||
string bucket,
|
||||
string expectedCompany)
|
||||
{
|
||||
await using var context = NewContext();
|
||||
await SeedBoundaryVendors(context);
|
||||
var service = new VendorDataService(context);
|
||||
|
||||
var (items, totalCount) = await service.GetPagedAsync(
|
||||
1,
|
||||
12,
|
||||
isActive: true,
|
||||
jobBuckets: new[] { bucket });
|
||||
|
||||
totalCount.Should().Be(1);
|
||||
items.Single().CompanyName.Should().Be(expectedCompany);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPagedAsync_UsesOrWithinFacetAndAndAcrossFacets()
|
||||
{
|
||||
await using var context = NewContext();
|
||||
await SeedBoundaryVendors(context);
|
||||
var service = new VendorDataService(context);
|
||||
|
||||
var (items, totalCount) = await service.GetPagedAsync(
|
||||
1,
|
||||
12,
|
||||
isActive: true,
|
||||
companies: new[] { "Under Co", "Hundred Co" },
|
||||
trades: new[] { "HVAC", "Plumbing" },
|
||||
locations: new[] { "Chicago, IL" },
|
||||
jobBuckets: new[] { "under-50", "100-149" });
|
||||
|
||||
totalCount.Should().Be(1);
|
||||
items.Single().CompanyName.Should().Be("Under Co");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPagedAsync_StatusAndCountAreIndependentOfInactiveHistory()
|
||||
{
|
||||
await using var context = NewContext();
|
||||
context.Vendors.AddRange(
|
||||
VendorWithJobs("Active Co", "Chicago", "IL", "Plumbing", 1),
|
||||
VendorWithJobs("Inactive Co", "Chicago", "IL", "Plumbing", 1, false));
|
||||
await context.SaveChangesAsync();
|
||||
var service = new VendorDataService(context);
|
||||
|
||||
var active = await service.GetPagedAsync(1, 1, isActive: true);
|
||||
var inactive = await service.GetPagedAsync(1, 1, isActive: false);
|
||||
|
||||
active.TotalCount.Should().Be(1);
|
||||
inactive.TotalCount.Should().Be(1);
|
||||
active.Items.Single().CompanyName.Should().Be("Active Co");
|
||||
inactive.Items.Single().CompanyName.Should().Be("Inactive Co");
|
||||
}
|
||||
}
|
||||
213
Api.SeaHavenIndustries.Tests/VendorServiceTests.cs
Normal file
213
Api.SeaHavenIndustries.Tests/VendorServiceTests.cs
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Implementation;
|
||||
using SeaHaven.Services.Validation;
|
||||
using Xunit;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Tests;
|
||||
|
||||
public class VendorServiceTests
|
||||
{
|
||||
private static VendorService NewService(Mock<IVendorDataService> data) =>
|
||||
new(data.Object, new CreateVendorValidation(), new UpdateVendorValidation());
|
||||
|
||||
[Fact]
|
||||
public async Task CreateVendor_PersistsCompleteProfile()
|
||||
{
|
||||
var data = new Mock<IVendorDataService>();
|
||||
Vendor? saved = null;
|
||||
data.Setup(x => x.AddAsync(It.IsAny<Vendor>()))
|
||||
.ReturnsAsync((Vendor vendor) => { saved = vendor; vendor.Id = 7; return vendor; });
|
||||
|
||||
var result = await NewService(data).CreateVendorAsync(new CreateVendorDTO
|
||||
{
|
||||
Name = "Gateway Plumbing",
|
||||
ContactName = "Jordan Lee",
|
||||
Email = "dispatch@gateway.example",
|
||||
Phone = "555-0100",
|
||||
CompanyPhone = "555-0199",
|
||||
Address = "10 Lake St",
|
||||
City = "Chicago",
|
||||
State = "IL",
|
||||
Zipcode = "60601",
|
||||
TradeSpecialties = "Plumbing, Backflow",
|
||||
GoogleMapsUrl = "https://maps.google.com/example",
|
||||
Notes = "After-hours dispatch available",
|
||||
IsActive = true
|
||||
}, "42");
|
||||
|
||||
saved.Should().NotBeNull();
|
||||
saved!.ContactName.Should().Be("Jordan Lee");
|
||||
saved.TradeSpecialties.Should().Be("Plumbing, Backflow");
|
||||
saved.CompanyPhone.Should().Be("555-0199");
|
||||
saved.GoogleMapsUrl.Should().Be("https://maps.google.com/example");
|
||||
saved.Notes.Should().Be("After-hours dispatch available");
|
||||
saved.City.Should().Be("Chicago");
|
||||
result.IsActive.Should().BeTrue();
|
||||
result.ContactName.Should().Be("Jordan Lee");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateVendor_UpdatesProfileAndActiveState()
|
||||
{
|
||||
var existing = new Vendor { Id = 8, CompanyName = "Old", IsActive = true };
|
||||
var data = new Mock<IVendorDataService>();
|
||||
data.Setup(x => x.GetByIdAsync(8)).ReturnsAsync(existing);
|
||||
data.Setup(x => x.UpdateAsync(existing)).Returns(Task.CompletedTask);
|
||||
|
||||
var result = await NewService(data).UpdateVendorAsync(8, new UpdateVendorDTO
|
||||
{
|
||||
Name = "New",
|
||||
ContactName = "Casey",
|
||||
TradeSpecialties = "HVAC",
|
||||
IsActive = false
|
||||
}, "42");
|
||||
|
||||
existing.CompanyName.Should().Be("New");
|
||||
existing.ContactName.Should().Be("Casey");
|
||||
existing.TradeSpecialties.Should().Be("HVAC");
|
||||
existing.IsActive.Should().BeFalse();
|
||||
existing.LastModifierUserId.Should().Be(42);
|
||||
result.IsActive.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteVendor_DeactivatesWithoutDeletingHistory()
|
||||
{
|
||||
var existing = new Vendor { Id = 9, CompanyName = "Keep History", IsActive = true };
|
||||
var data = new Mock<IVendorDataService>();
|
||||
data.Setup(x => x.GetByIdAsync(9)).ReturnsAsync(existing);
|
||||
data.Setup(x => x.UpdateAsync(existing)).Returns(Task.CompletedTask);
|
||||
|
||||
await NewService(data).DeleteVendorAsync(9, "42");
|
||||
|
||||
existing.IsActive.Should().BeFalse();
|
||||
existing.IsDeleted.Should().NotBeTrue();
|
||||
existing.DeletionTime.Should().BeNull();
|
||||
existing.LastModificationTime.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetVendorsPaged_RequestsSelectedStatusAndMapsDirectoryFields()
|
||||
{
|
||||
var data = new Mock<IVendorDataService>();
|
||||
data.Setup(x => x.GetPagedAsync(
|
||||
1,
|
||||
12,
|
||||
"chicago",
|
||||
false,
|
||||
It.IsAny<IReadOnlyCollection<string>?>(),
|
||||
It.IsAny<IReadOnlyCollection<string>?>(),
|
||||
It.IsAny<IReadOnlyCollection<string>?>(),
|
||||
It.IsAny<IReadOnlyCollection<string>?>()))
|
||||
.ReturnsAsync((new[]
|
||||
{
|
||||
new Vendor
|
||||
{
|
||||
Id = 10,
|
||||
CompanyName = "Inactive Vendor",
|
||||
ContactName = "Taylor",
|
||||
City = "Chicago",
|
||||
State = "IL",
|
||||
TradeSpecialties = "Electrical",
|
||||
IsActive = false
|
||||
}
|
||||
}.AsEnumerable(), 1));
|
||||
|
||||
var result = await NewService(data).GetVendorsPagedAsync(1, 12, "chicago", false);
|
||||
|
||||
result.TotalCount.Should().Be(1);
|
||||
result.Items.Single().Should().BeEquivalentTo(new
|
||||
{
|
||||
Id = 10,
|
||||
Name = "Inactive Vendor",
|
||||
ContactName = "Taylor",
|
||||
City = "Chicago",
|
||||
State = "IL",
|
||||
TradeSpecialties = "Electrical",
|
||||
IsActive = false
|
||||
}, options => options.ExcludingMissingMembers());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetVendorById_CountsDistinctHistoricalWorkOrders()
|
||||
{
|
||||
var vendor = new Vendor
|
||||
{
|
||||
Id = 11,
|
||||
CompanyName = "History Vendor",
|
||||
Dispatches = new List<Dispatch>
|
||||
{
|
||||
new()
|
||||
{
|
||||
WorkOrderId = 100,
|
||||
DispatchWorkOrders = new List<DispatchWorkOrder>
|
||||
{
|
||||
new() { WorkOrderId = 100 },
|
||||
new() { WorkOrderId = 101 }
|
||||
}
|
||||
},
|
||||
new() { WorkOrderId = 101 }
|
||||
}
|
||||
};
|
||||
var data = new Mock<IVendorDataService>();
|
||||
data.Setup(x => x.GetByIdAsync(11)).ReturnsAsync(vendor);
|
||||
|
||||
var result = await NewService(data).GetVendorByIdAsync(11);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.TotalJobs.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetVendorsPaged_ForwardsFacetFilters()
|
||||
{
|
||||
var companies = new[] { "Gateway Plumbing", "Roto-Rooter Dallas" };
|
||||
var trades = new[] { "Backflow Preventers" };
|
||||
var locations = new[] { "Dallas, TX" };
|
||||
var jobBuckets = new[] { "100-149" };
|
||||
var data = new Mock<IVendorDataService>();
|
||||
data.Setup(x => x.GetPagedAsync(
|
||||
1,
|
||||
25,
|
||||
"gateway",
|
||||
true,
|
||||
companies,
|
||||
trades,
|
||||
locations,
|
||||
jobBuckets))
|
||||
.ReturnsAsync((Enumerable.Empty<Vendor>(), 0));
|
||||
|
||||
await NewService(data).GetVendorsPagedAsync(
|
||||
1,
|
||||
25,
|
||||
"gateway",
|
||||
true,
|
||||
companies,
|
||||
trades,
|
||||
locations,
|
||||
jobBuckets);
|
||||
|
||||
data.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateVendor_RequiresTechnicianNameAndHttpsMapUrl()
|
||||
{
|
||||
var validator = new CreateVendorValidation();
|
||||
|
||||
var result = validator.Validate(new CreateVendorDTO
|
||||
{
|
||||
Name = "Gateway Plumbing",
|
||||
ContactName = "",
|
||||
GoogleMapsUrl = "http://maps.google.com/gateway"
|
||||
});
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(error => error.PropertyName == nameof(CreateVendorDTO.ContactName));
|
||||
result.Errors.Should().Contain(error => error.PropertyName == nameof(CreateVendorDTO.GoogleMapsUrl));
|
||||
}
|
||||
}
|
||||
|
|
@ -32,16 +32,43 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
}
|
||||
|
||||
[HttpGet("GetVendorList")]
|
||||
public async Task<IActionResult> GetVendorList(string? search = "", int page = 1, int pageSize = 10)
|
||||
public async Task<IActionResult> GetVendorList(
|
||||
string? search = "",
|
||||
int page = 1,
|
||||
int pageSize = 10,
|
||||
bool? isActive = true,
|
||||
[FromQuery] string[]? companies = null,
|
||||
[FromQuery] string[]? trades = null,
|
||||
[FromQuery] string[]? locations = null,
|
||||
[FromQuery] string[]? jobBuckets = null)
|
||||
{
|
||||
var pagedResult = await _vendorService.GetVendorsPagedAsync(page, pageSize, search);
|
||||
var pagedResult = await _vendorService.GetVendorsPagedAsync(
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
isActive,
|
||||
companies,
|
||||
trades,
|
||||
locations,
|
||||
jobBuckets);
|
||||
|
||||
var data = pagedResult.Items.Select(v => new
|
||||
{
|
||||
v.Id,
|
||||
CompanyName = v.Name,
|
||||
v.ContactName,
|
||||
v.Email,
|
||||
v.Phone
|
||||
v.Phone,
|
||||
v.CompanyPhone,
|
||||
v.Address,
|
||||
v.City,
|
||||
v.State,
|
||||
Zip = v.Zipcode,
|
||||
v.TradeSpecialties,
|
||||
v.GoogleMapsUrl,
|
||||
v.Notes,
|
||||
v.IsActive,
|
||||
v.TotalJobs
|
||||
});
|
||||
|
||||
return Ok(new Pagination_DTO
|
||||
|
|
@ -56,17 +83,35 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
|
||||
[HttpGet("{id}")]
|
||||
[HttpGet("GetById")]
|
||||
public async Task<IActionResult> GetVendorById([FromQuery] int? id)
|
||||
public async Task<IActionResult> GetVendorById([FromRoute] int? id, [FromQuery(Name = "id")] int? queryId)
|
||||
{
|
||||
if (id == null)
|
||||
var vendorId = id ?? queryId;
|
||||
if (vendorId == null)
|
||||
return BadRequest(new Response { Status = "Error", Message = "Id is required" });
|
||||
|
||||
var vendor = await _vendorService.GetVendorByIdAsync(id.Value);
|
||||
var vendor = await _vendorService.GetVendorByIdAsync(vendorId.Value);
|
||||
|
||||
if (vendor == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
|
||||
|
||||
return Ok(vendor);
|
||||
return Ok(new
|
||||
{
|
||||
vendor.Id,
|
||||
CompanyName = vendor.Name,
|
||||
vendor.ContactName,
|
||||
vendor.Email,
|
||||
vendor.Phone,
|
||||
vendor.CompanyPhone,
|
||||
vendor.Address,
|
||||
vendor.City,
|
||||
vendor.State,
|
||||
Zip = vendor.Zipcode,
|
||||
vendor.TradeSpecialties,
|
||||
vendor.GoogleMapsUrl,
|
||||
vendor.Notes,
|
||||
vendor.IsActive,
|
||||
vendor.TotalJobs
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
|
@ -77,7 +122,19 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
var createDto = new CreateVendorDTO
|
||||
{
|
||||
Name = model.CompanyName ?? throw new ArgumentException("CompanyName is required")
|
||||
Name = model.CompanyName ?? throw new ArgumentException("CompanyName is required"),
|
||||
ContactName = model.ContactName,
|
||||
Email = model.Email,
|
||||
Phone = model.Phone,
|
||||
CompanyPhone = model.CompanyPhone,
|
||||
Address = model.Address,
|
||||
City = model.City,
|
||||
State = model.State,
|
||||
Zipcode = model.Zip,
|
||||
TradeSpecialties = model.TradeSpecialties,
|
||||
GoogleMapsUrl = model.GoogleMapsUrl,
|
||||
Notes = model.Notes,
|
||||
IsActive = model.IsActive
|
||||
};
|
||||
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
|
@ -111,7 +168,19 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
|
||||
var updateDto = new UpdateVendorDTO
|
||||
{
|
||||
Name = model.CompanyName
|
||||
Name = model.CompanyName,
|
||||
ContactName = model.ContactName,
|
||||
Email = model.Email,
|
||||
Phone = model.Phone,
|
||||
CompanyPhone = model.CompanyPhone,
|
||||
Address = model.Address,
|
||||
City = model.City,
|
||||
State = model.State,
|
||||
Zipcode = model.Zip,
|
||||
TradeSpecialties = model.TradeSpecialties,
|
||||
GoogleMapsUrl = model.GoogleMapsUrl,
|
||||
Notes = model.Notes,
|
||||
IsActive = model.IsActive
|
||||
};
|
||||
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
|
@ -126,7 +195,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
|
||||
}
|
||||
|
|
@ -138,7 +207,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("Delete")]
|
||||
public async Task<IActionResult> Delete([FromRoute] int? id, [FromQuery] int? queryId = null)
|
||||
public async Task<IActionResult> Delete([FromRoute] int? id, [FromQuery(Name = "id")] int? queryId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -152,9 +221,9 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _vendorService.DeleteVendorAsync(vendorId, userId);
|
||||
return Ok(new DataResponse { Message = "Vendor Deleted", Status = "200" });
|
||||
return Ok(new DataResponse { Message = "Vendor Deactivated", Status = "200" });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
|
||||
}
|
||||
|
|
@ -212,6 +281,59 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("facets")]
|
||||
public async Task<IActionResult> GetFacets()
|
||||
{
|
||||
var vendors = await _db.Vendors
|
||||
.AsNoTracking()
|
||||
.Where(v => v.IsDeleted == null || v.IsDeleted == false)
|
||||
.OrderByDescending(v => v.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var companies = vendors
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v.CompanyName))
|
||||
.GroupBy(v => v.CompanyName!.Trim(), StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => group.First())
|
||||
.OrderBy(v => v.CompanyName)
|
||||
.Select(v => new
|
||||
{
|
||||
Name = v.CompanyName,
|
||||
v.CompanyPhone,
|
||||
v.Email,
|
||||
v.Address,
|
||||
v.City,
|
||||
v.State,
|
||||
Zip = v.Zip,
|
||||
v.GoogleMapsUrl
|
||||
});
|
||||
|
||||
var trades = vendors
|
||||
.SelectMany(v => (v.TradeSpecialties ?? "")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(value => value);
|
||||
|
||||
var locations = vendors
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v.City) || !string.IsNullOrWhiteSpace(v.State))
|
||||
.Select(v => new { v.City, v.State, Label = string.Join(", ", new[] { v.City, v.State }.Where(value => !string.IsNullOrWhiteSpace(value))) })
|
||||
.DistinctBy(location => location.Label, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(location => location.Label);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Companies = companies,
|
||||
Trades = trades,
|
||||
Locations = locations,
|
||||
JobBuckets = new[]
|
||||
{
|
||||
new { Id = "under-50", Label = "Under 50" },
|
||||
new { Id = "50-99", Label = "50–99" },
|
||||
new { Id = "100-149", Label = "100–149" },
|
||||
new { Id = "150-plus", Label = "150+" }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/portal-token")]
|
||||
public async Task<IActionResult> GetPortalToken(int id)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@ namespace Api.SeaHavenIndustries.DTOs
|
|||
public string? ContactName { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? CompanyPhone { 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 string? GoogleMapsUrl { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260720220000_AddVendorDirectoryDetails")]
|
||||
public partial class AddVendorDirectoryDetails : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CompanyPhone",
|
||||
table: "Vendors",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "GoogleMapsUrl",
|
||||
table: "Vendors",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Notes",
|
||||
table: "Vendors",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "CompanyPhone", table: "Vendors");
|
||||
migrationBuilder.DropColumn(name: "GoogleMapsUrl", table: "Vendors");
|
||||
migrationBuilder.DropColumn(name: "Notes", table: "Vendors");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1811,6 +1811,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("CompanyName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("CompanyPhone")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ContactName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
@ -1826,6 +1829,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Email")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("GoogleMapsUrl")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
|
|
@ -1838,6 +1844,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@ namespace Data.SeaHavenIndustries
|
|||
public string? ContactName { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? CompanyPhone { 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 string? GoogleMapsUrl { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public ICollection<Dispatch>? Dispatches { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
|
|
@ -15,12 +16,17 @@ namespace SeaHaven.DataServices.Implementation
|
|||
|
||||
public async Task<Vendor?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.Vendors.FindAsync(id);
|
||||
return await _context.Vendors
|
||||
.Include(v => v.Dispatches!)
|
||||
.ThenInclude(dispatch => dispatch.DispatchWorkOrders)
|
||||
.FirstOrDefaultAsync(v => v.Id == id);
|
||||
}
|
||||
|
||||
public async Task<Vendor?> GetByIdWithDetailsAsync(int id)
|
||||
{
|
||||
return await _context.Vendors
|
||||
.Include(v => v.Dispatches!)
|
||||
.ThenInclude(dispatch => dispatch.DispatchWorkOrders)
|
||||
.FirstOrDefaultAsync(v => v.Id == id);
|
||||
}
|
||||
|
||||
|
|
@ -32,11 +38,19 @@ namespace SeaHaven.DataServices.Implementation
|
|||
public async Task<(IEnumerable<Vendor> Items, int TotalCount)> GetPagedAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
string? search = null)
|
||||
string? search = null,
|
||||
bool? isActive = true,
|
||||
IReadOnlyCollection<string>? companies = null,
|
||||
IReadOnlyCollection<string>? trades = null,
|
||||
IReadOnlyCollection<string>? locations = null,
|
||||
IReadOnlyCollection<string>? jobBuckets = null)
|
||||
{
|
||||
search ??= "";
|
||||
|
||||
var query = _context.Vendors.Where(v => v.IsActive);
|
||||
var query = _context.Vendors.Where(v => v.IsDeleted == null || v.IsDeleted == false);
|
||||
|
||||
if (isActive.HasValue)
|
||||
query = query.Where(v => v.IsActive == isActive.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
|
|
@ -45,11 +59,71 @@ namespace SeaHaven.DataServices.Implementation
|
|||
(v.CompanyName ?? "").ToLower().Contains(s) ||
|
||||
(v.ContactName ?? "").ToLower().Contains(s) ||
|
||||
(v.Email ?? "").ToLower().Contains(s) ||
|
||||
(v.TradeSpecialties ?? "").ToLower().Contains(s));
|
||||
(v.TradeSpecialties ?? "").ToLower().Contains(s) ||
|
||||
(v.City ?? "").ToLower().Contains(s) ||
|
||||
(v.State ?? "").ToLower().Contains(s) ||
|
||||
(v.Zip ?? "").ToLower().Contains(s));
|
||||
}
|
||||
|
||||
var selectedCompanies = companies?
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(value => value.Trim().ToLower())
|
||||
.Distinct()
|
||||
.ToArray() ?? Array.Empty<string>();
|
||||
if (selectedCompanies.Length > 0)
|
||||
query = query.Where(v => selectedCompanies.Contains((v.CompanyName ?? "").ToLower()));
|
||||
|
||||
var selectedTrades = trades?
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(value => value.Trim().ToLower())
|
||||
.Distinct()
|
||||
.ToArray() ?? Array.Empty<string>();
|
||||
if (selectedTrades.Length > 0)
|
||||
query = query.Where(BuildTradePredicate(selectedTrades));
|
||||
|
||||
var selectedLocations = locations?
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(value => value.Trim().ToLower())
|
||||
.Distinct()
|
||||
.ToArray() ?? Array.Empty<string>();
|
||||
if (selectedLocations.Length > 0)
|
||||
query = query.Where(v => selectedLocations.Contains(((v.City ?? "") + ", " + (v.State ?? "")).ToLower()));
|
||||
|
||||
var selectedJobBuckets = jobBuckets?
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(value => value.Trim().ToLower())
|
||||
.Distinct()
|
||||
.ToArray() ?? Array.Empty<string>();
|
||||
if (selectedJobBuckets.Length > 0)
|
||||
{
|
||||
var under50 = selectedJobBuckets.Contains("under-50");
|
||||
var from50To99 = selectedJobBuckets.Contains("50-99");
|
||||
var from100To149 = selectedJobBuckets.Contains("100-149");
|
||||
var from150 = selectedJobBuckets.Contains("150-plus");
|
||||
|
||||
var vendorsWithDispatches = await query
|
||||
.Include(v => v.Dispatches!)
|
||||
.ThenInclude(dispatch => dispatch.DispatchWorkOrders)
|
||||
.OrderByDescending(v => v.Id)
|
||||
.ToListAsync();
|
||||
var filtered = vendorsWithDispatches.Where(vendor =>
|
||||
{
|
||||
var totalJobs = CountDistinctWorkOrders(vendor);
|
||||
return (under50 && totalJobs < 50) ||
|
||||
(from50To99 && totalJobs is >= 50 and <= 99) ||
|
||||
(from100To149 && totalJobs is >= 100 and <= 149) ||
|
||||
(from150 && totalJobs >= 150);
|
||||
}).ToList();
|
||||
|
||||
return (
|
||||
filtered.Skip((page - 1) * pageSize).Take(pageSize),
|
||||
filtered.Count);
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
var items = await query
|
||||
.Include(v => v.Dispatches!)
|
||||
.ThenInclude(dispatch => dispatch.DispatchWorkOrders)
|
||||
.OrderByDescending(v => v.Id)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
|
|
@ -58,6 +132,47 @@ namespace SeaHaven.DataServices.Implementation
|
|||
return (items, totalCount);
|
||||
}
|
||||
|
||||
private static int CountDistinctWorkOrders(Vendor vendor)
|
||||
{
|
||||
if (vendor.Dispatches == null)
|
||||
return 0;
|
||||
|
||||
return vendor.Dispatches
|
||||
.SelectMany(dispatch =>
|
||||
{
|
||||
var linked = dispatch.DispatchWorkOrders?.Select(item => item.WorkOrderId)
|
||||
?? Enumerable.Empty<int>();
|
||||
return dispatch.WorkOrderId.HasValue
|
||||
? linked.Append(dispatch.WorkOrderId.Value)
|
||||
: linked;
|
||||
})
|
||||
.Distinct()
|
||||
.Count();
|
||||
}
|
||||
|
||||
private static Expression<Func<Vendor, bool>> BuildTradePredicate(
|
||||
IReadOnlyCollection<string> selectedTrades)
|
||||
{
|
||||
var vendor = Expression.Parameter(typeof(Vendor), "vendor");
|
||||
var specialties = Expression.Coalesce(
|
||||
Expression.Property(vendor, nameof(Vendor.TradeSpecialties)),
|
||||
Expression.Constant(string.Empty));
|
||||
var lowered = Expression.Call(specialties, nameof(string.ToLower), Type.EmptyTypes);
|
||||
Expression? body = null;
|
||||
|
||||
foreach (var trade in selectedTrades)
|
||||
{
|
||||
var contains = Expression.Call(
|
||||
lowered,
|
||||
nameof(string.Contains),
|
||||
Type.EmptyTypes,
|
||||
Expression.Constant(trade));
|
||||
body = body == null ? contains : Expression.OrElse(body, contains);
|
||||
}
|
||||
|
||||
return Expression.Lambda<Func<Vendor, bool>>(body ?? Expression.Constant(false), vendor);
|
||||
}
|
||||
|
||||
public async Task<Vendor> AddAsync(Vendor vendor)
|
||||
{
|
||||
await _context.Vendors.AddAsync(vendor);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,15 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
Task<Vendor?> GetByIdAsync(int id);
|
||||
Task<Vendor?> GetByIdWithDetailsAsync(int id);
|
||||
Task<IEnumerable<Vendor>> GetAllAsync();
|
||||
Task<(IEnumerable<Vendor> Items, int TotalCount)> GetPagedAsync(int page, int pageSize, string? search = null);
|
||||
Task<(IEnumerable<Vendor> Items, int TotalCount)> GetPagedAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
string? search = null,
|
||||
bool? isActive = true,
|
||||
IReadOnlyCollection<string>? companies = null,
|
||||
IReadOnlyCollection<string>? trades = null,
|
||||
IReadOnlyCollection<string>? locations = null,
|
||||
IReadOnlyCollection<string>? jobBuckets = null);
|
||||
Task<Vendor> AddAsync(Vendor vendor);
|
||||
Task UpdateAsync(Vendor vendor);
|
||||
Task DeleteAsync(int id);
|
||||
|
|
|
|||
|
|
@ -4,33 +4,52 @@ namespace SeaHaven.Services.DTOs
|
|||
{
|
||||
public int Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? ContactName { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? CompanyPhone { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? State { get; set; }
|
||||
public string? Zipcode { get; set; }
|
||||
public string? TradeSpecialties { get; set; }
|
||||
public string? GoogleMapsUrl { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public int TotalJobs { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class CreateVendorDTO
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public string? ContactName { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? CompanyPhone { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? State { get; set; }
|
||||
public string? Zipcode { get; set; }
|
||||
public string? TradeSpecialties { get; set; }
|
||||
public string? GoogleMapsUrl { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
public class UpdateVendorDTO
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? ContactName { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? CompanyPhone { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? State { get; set; }
|
||||
public string? Zipcode { get; set; }
|
||||
public string? TradeSpecialties { get; set; }
|
||||
public string? GoogleMapsUrl { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool? IsActive { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,9 +45,25 @@ namespace SeaHaven.Services.Implementation
|
|||
return vendors.Select(MapToDTO);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<VendorDTO>> GetVendorsPagedAsync(int page, int pageSize, string? search = null)
|
||||
public async Task<PagedResult<VendorDTO>> GetVendorsPagedAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
string? search = null,
|
||||
bool? isActive = true,
|
||||
IReadOnlyCollection<string>? companies = null,
|
||||
IReadOnlyCollection<string>? trades = null,
|
||||
IReadOnlyCollection<string>? locations = null,
|
||||
IReadOnlyCollection<string>? jobBuckets = null)
|
||||
{
|
||||
var (items, totalCount) = await _vendorDataService.GetPagedAsync(page, pageSize, search);
|
||||
var (items, totalCount) = await _vendorDataService.GetPagedAsync(
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
isActive,
|
||||
companies,
|
||||
trades,
|
||||
locations,
|
||||
jobBuckets);
|
||||
return new PagedResult<VendorDTO>
|
||||
{
|
||||
Items = items.Select(MapToDTO).ToList(),
|
||||
|
|
@ -68,12 +84,18 @@ namespace SeaHaven.Services.Implementation
|
|||
var vendor = new Vendor
|
||||
{
|
||||
CompanyName = dto.Name,
|
||||
ContactName = dto.ContactName,
|
||||
Email = dto.Email,
|
||||
Phone = dto.Phone,
|
||||
CompanyPhone = dto.CompanyPhone,
|
||||
Address = dto.Address,
|
||||
City = dto.City,
|
||||
State = dto.State,
|
||||
Zip = dto.Zipcode,
|
||||
TradeSpecialties = dto.TradeSpecialties,
|
||||
GoogleMapsUrl = dto.GoogleMapsUrl,
|
||||
Notes = dto.Notes,
|
||||
IsActive = dto.IsActive,
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
createdby = userId
|
||||
};
|
||||
|
|
@ -95,12 +117,18 @@ namespace SeaHaven.Services.Implementation
|
|||
throw new InvalidOperationException($"Vendor with ID {id} not found");
|
||||
|
||||
if (dto.Name != null) vendor.CompanyName = dto.Name;
|
||||
if (dto.ContactName != null) vendor.ContactName = dto.ContactName;
|
||||
if (dto.Email != null) vendor.Email = dto.Email;
|
||||
if (dto.Phone != null) vendor.Phone = dto.Phone;
|
||||
if (dto.CompanyPhone != null) vendor.CompanyPhone = dto.CompanyPhone;
|
||||
if (dto.Address != null) vendor.Address = dto.Address;
|
||||
if (dto.City != null) vendor.City = dto.City;
|
||||
if (dto.State != null) vendor.State = dto.State;
|
||||
if (dto.Zipcode != null) vendor.Zip = dto.Zipcode;
|
||||
if (dto.TradeSpecialties != null) vendor.TradeSpecialties = dto.TradeSpecialties;
|
||||
if (dto.GoogleMapsUrl != null) vendor.GoogleMapsUrl = dto.GoogleMapsUrl;
|
||||
if (dto.Notes != null) vendor.Notes = dto.Notes;
|
||||
if (dto.IsActive.HasValue) vendor.IsActive = dto.IsActive.Value;
|
||||
|
||||
vendor.LastModificationTime = DateTime.UtcNow;
|
||||
if (int.TryParse(userId, out int userIdInt))
|
||||
|
|
@ -116,9 +144,10 @@ namespace SeaHaven.Services.Implementation
|
|||
if (vendor == null)
|
||||
throw new InvalidOperationException($"Vendor with ID {id} not found");
|
||||
|
||||
vendor.IsDeleted = true;
|
||||
vendor.DeleterUserId = userId;
|
||||
vendor.DeletionTime = DateTime.UtcNow;
|
||||
vendor.IsActive = false;
|
||||
vendor.LastModificationTime = DateTime.UtcNow;
|
||||
if (int.TryParse(userId, out int userIdInt))
|
||||
vendor.LastModifierUserId = userIdInt;
|
||||
|
||||
await _vendorDataService.UpdateAsync(vendor);
|
||||
}
|
||||
|
|
@ -139,13 +168,38 @@ namespace SeaHaven.Services.Implementation
|
|||
{
|
||||
Id = vendor.Id,
|
||||
Name = vendor.CompanyName,
|
||||
ContactName = vendor.ContactName,
|
||||
Email = vendor.Email,
|
||||
Phone = vendor.Phone,
|
||||
CompanyPhone = vendor.CompanyPhone,
|
||||
Address = vendor.Address,
|
||||
City = vendor.City,
|
||||
State = vendor.State,
|
||||
Zipcode = vendor.Zip
|
||||
Zipcode = vendor.Zip,
|
||||
TradeSpecialties = vendor.TradeSpecialties,
|
||||
GoogleMapsUrl = vendor.GoogleMapsUrl,
|
||||
Notes = vendor.Notes,
|
||||
IsActive = vendor.IsActive,
|
||||
TotalJobs = CountDistinctWorkOrders(vendor)
|
||||
};
|
||||
}
|
||||
|
||||
private static int CountDistinctWorkOrders(Vendor vendor)
|
||||
{
|
||||
if (vendor.Dispatches == null)
|
||||
return 0;
|
||||
|
||||
return vendor.Dispatches
|
||||
.SelectMany(dispatch =>
|
||||
{
|
||||
var linked = dispatch.DispatchWorkOrders?.Select(item => item.WorkOrderId)
|
||||
?? Enumerable.Empty<int>();
|
||||
return dispatch.WorkOrderId.HasValue
|
||||
? linked.Append(dispatch.WorkOrderId.Value)
|
||||
: linked;
|
||||
})
|
||||
.Distinct()
|
||||
.Count();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,15 @@ namespace SeaHaven.Services.Interfaces
|
|||
Task<VendorDTO?> GetVendorByIdAsync(int id);
|
||||
Task<VendorDTO?> GetVendorByIdWithDetailsAsync(int id);
|
||||
Task<IEnumerable<VendorDTO>> GetAllVendorsAsync();
|
||||
Task<PagedResult<VendorDTO>> GetVendorsPagedAsync(int page, int pageSize, string? search = null);
|
||||
Task<PagedResult<VendorDTO>> GetVendorsPagedAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
string? search = null,
|
||||
bool? isActive = true,
|
||||
IReadOnlyCollection<string>? companies = null,
|
||||
IReadOnlyCollection<string>? trades = null,
|
||||
IReadOnlyCollection<string>? locations = null,
|
||||
IReadOnlyCollection<string>? jobBuckets = null);
|
||||
Task<VendorDTO> CreateVendorAsync(CreateVendorDTO dto, string userId);
|
||||
Task<VendorDTO> UpdateVendorAsync(int id, UpdateVendorDTO dto, string userId);
|
||||
Task DeleteVendorAsync(int id, string userId);
|
||||
|
|
|
|||
|
|
@ -19,10 +19,22 @@ namespace SeaHaven.Services.Validation
|
|||
.EmailAddress().WithMessage("Invalid email address")
|
||||
.When(x => !string.IsNullOrEmpty(x.Email));
|
||||
|
||||
RuleFor(x => x.ContactName)
|
||||
.NotEmpty().WithMessage("Technician name is required")
|
||||
.MaximumLength(200).WithMessage("Contact name cannot exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.TradeSpecialties)
|
||||
.MaximumLength(1000).WithMessage("Trade specialties cannot exceed 1000 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.TradeSpecialties));
|
||||
|
||||
RuleFor(x => x.Phone)
|
||||
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Phone));
|
||||
|
||||
RuleFor(x => x.CompanyPhone)
|
||||
.MaximumLength(20).WithMessage("Company phone cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.CompanyPhone));
|
||||
|
||||
RuleFor(x => x.Address)
|
||||
.MaximumLength(500).WithMessage("Address cannot exceed 500 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Address));
|
||||
|
|
@ -38,6 +50,14 @@ namespace SeaHaven.Services.Validation
|
|||
RuleFor(x => x.Zipcode)
|
||||
.MaximumLength(10).WithMessage("Zipcode cannot exceed 10 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Zipcode));
|
||||
|
||||
RuleFor(x => x.GoogleMapsUrl)
|
||||
.Must(VendorValidationRules.BeValidHttpsUrl).WithMessage("Google Maps URL must be a valid HTTPS URL")
|
||||
.When(x => !string.IsNullOrEmpty(x.GoogleMapsUrl));
|
||||
|
||||
RuleFor(x => x.Notes)
|
||||
.MaximumLength(2000).WithMessage("Notes cannot exceed 2000 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Notes));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,10 +77,23 @@ namespace SeaHaven.Services.Validation
|
|||
.EmailAddress().WithMessage("Invalid email address")
|
||||
.When(x => !string.IsNullOrEmpty(x.Email));
|
||||
|
||||
RuleFor(x => x.ContactName)
|
||||
.NotEmpty().WithMessage("Technician name is required")
|
||||
.MaximumLength(200).WithMessage("Contact name cannot exceed 200 characters")
|
||||
.When(x => x.ContactName != null);
|
||||
|
||||
RuleFor(x => x.TradeSpecialties)
|
||||
.MaximumLength(1000).WithMessage("Trade specialties cannot exceed 1000 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.TradeSpecialties));
|
||||
|
||||
RuleFor(x => x.Phone)
|
||||
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Phone));
|
||||
|
||||
RuleFor(x => x.CompanyPhone)
|
||||
.MaximumLength(20).WithMessage("Company phone cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.CompanyPhone));
|
||||
|
||||
RuleFor(x => x.Address)
|
||||
.MaximumLength(500).WithMessage("Address cannot exceed 500 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Address));
|
||||
|
|
@ -76,6 +109,21 @@ namespace SeaHaven.Services.Validation
|
|||
RuleFor(x => x.Zipcode)
|
||||
.MaximumLength(10).WithMessage("Zipcode cannot exceed 10 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Zipcode));
|
||||
|
||||
RuleFor(x => x.GoogleMapsUrl)
|
||||
.Must(VendorValidationRules.BeValidHttpsUrl).WithMessage("Google Maps URL must be a valid HTTPS URL")
|
||||
.When(x => !string.IsNullOrEmpty(x.GoogleMapsUrl));
|
||||
|
||||
RuleFor(x => x.Notes)
|
||||
.MaximumLength(2000).WithMessage("Notes cannot exceed 2000 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Notes));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal static class VendorValidationRules
|
||||
{
|
||||
internal static bool BeValidHttpsUrl(string? value) =>
|
||||
Uri.TryCreate(value, UriKind.Absolute, out var uri) && uri.Scheme == Uri.UriSchemeHttps;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue