diff --git a/Api.SeaHavenIndustries.Tests/VendorOperationsControllerTests.cs b/Api.SeaHavenIndustries.Tests/VendorOperationsControllerTests.cs new file mode 100644 index 0000000..d7d7bec --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/VendorOperationsControllerTests.cs @@ -0,0 +1,313 @@ +using System.Collections; +using System.Security.Claims; +using Api.SeaHavenIndustries.Controllers; +using Data.SeaHavenIndustries; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Implementation; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public class VendorOperationsControllerTests +{ + private static ApplicationDbContext NewContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new ApplicationDbContext(options); + } + + private static VendorOperationsController NewController(ApplicationDbContext context) + { + var controller = new VendorOperationsController( + context, + new DispatchDataService(context), + TimeProvider.System); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + new[] { new Claim(ClaimTypes.NameIdentifier, "test-user") }, "test")) + } + }; + return controller; + } + + private static object Prop(object source, string name) => + source.GetType().GetProperty(name)!.GetValue(source)!; + + [Fact] + public async Task Notifications_ReturnsUnassignedWorkOrderInsideFortyEightHours() + { + using var context = NewContext(); + context.workOrders.Add(new WorkOrder + { + InternalWONumber = "WO-48", + WorkerOrderTitle = "Needs vendor", + ScheduledDate = DateTime.UtcNow.AddHours(47) + }); + await context.SaveChangesAsync(); + + var result = await NewController(context).GetNotifications(); + + var value = ((OkObjectResult)result).Value!; + var items = ((IEnumerable)Prop(value, "Items")).Cast().ToList(); + items.Should().ContainSingle(); + Prop(items[0], "Type").Should().Be("NoVendor"); + Prop(items[0], "Title").Should().Be("No vendor assigned — 48h to service"); + } + + [Fact] + public async Task Notifications_RemovesReminderAsSoonAsVendorIsAssigned() + { + using var context = NewContext(); + var vendor = new Vendor { CompanyName = "Gateway", IsActive = true }; + var workOrder = new WorkOrder + { + InternalWONumber = "WO-ASSIGNED", + ScheduledDate = DateTime.UtcNow.AddHours(24) + }; + context.AddRange(vendor, workOrder); + await context.SaveChangesAsync(); + context.Dispatches.Add(new Dispatch + { + VendorId = vendor.Id, + WorkOrderId = workOrder.Id, + Status = "Sent", + ScheduledDate = workOrder.ScheduledDate + }); + await context.SaveChangesAsync(); + + var result = await NewController(context).GetNotifications(); + + var items = ((IEnumerable)Prop(((OkObjectResult)result).Value!, "Items")).Cast(); + items.Should().BeEmpty(); + } + + [Fact] + public async Task Availability_SurfacesOverlapAndRanksPreferredVendorFirst() + { + using var context = NewContext(); + var site = new Locations { Name = "St Louis" }; + var preferred = new Vendor + { + CompanyName = "Preferred", + ContactName = "Alex", + IsActive = true, + TradeSpecialties = "HVAC", + AvailabilityStatus = "Available", + AvailabilityUpdatedAt = DateTime.UtcNow + }; + var busy = new Vendor { CompanyName = "Busy", ContactName = "Sam", IsActive = true }; + var workOrder = new WorkOrder + { + InternalWONumber = "WO-BUSY", + ScheduledStart = DateTime.UtcNow.AddHours(1), + ScheduledEnd = DateTime.UtcNow.AddHours(3) + }; + context.AddRange(site, preferred, busy, workOrder); + await context.SaveChangesAsync(); + context.SitePreferredVendors.Add(new SitePreferredVendor + { + LocationId = site.Id, + VendorId = preferred.Id, + Trade = "HVAC", + SortOrder = 0 + }); + context.Dispatches.Add(new Dispatch + { + VendorId = busy.Id, + WorkOrderId = workOrder.Id, + Status = "Sent", + ScheduledDate = workOrder.ScheduledStart + }); + await context.SaveChangesAsync(); + + var result = await NewController(context).GetAvailability( + DateTime.UtcNow.AddHours(2), + DateTime.UtcNow.AddHours(4), + site.Id, + "HVAC"); + + var rows = ((IEnumerable)Prop(((OkObjectResult)result).Value!, "Data")).Cast().ToList(); + Prop(rows[0], "CompanyName").Should().Be("Preferred"); + Prop(rows[0], "IsPreferred").Should().Be(true); + var busyRow = rows.Single(row => (string?)Prop(row, "CompanyName") == "Busy"); + Prop(busyRow, "AvailabilityStatus").Should().Be("Unavailable"); + } + + [Fact] + public async Task ReplaceSitePreferences_RejectsInactiveVendor() + { + using var context = NewContext(); + var site = new Locations { Name = "St Louis" }; + var inactive = new Vendor { CompanyName = "Inactive", IsActive = false }; + context.AddRange(site, inactive); + await context.SaveChangesAsync(); + + var result = await NewController(context).ReplaceSitePreferences(site.Id, + new List + { + new() { VendorId = inactive.Id } + }); + + result.Should().BeOfType(); + context.SitePreferredVendors.Should().BeEmpty(); + } + + [Fact] + public async Task InsightsExports_ReturnStableCsvAndPdfFormats() + { + using var context = NewContext(); + var vendor = new Vendor { CompanyName = "=HYPERLINK(\"https://invalid.test\")", IsActive = false }; + var workOrder = new WorkOrder { DueDate = DateTime.UtcNow.AddHours(1), Trade = "Plumbing" }; + context.AddRange(vendor, workOrder); + await context.SaveChangesAsync(); + context.Dispatches.Add(new Dispatch + { + VendorId = vendor.Id, + WorkOrderId = workOrder.Id, + Status = "Completed", + DispatchedAt = DateTime.UtcNow.AddHours(-4), + ScheduledDate = DateTime.UtcNow.AddHours(-2), + AcknowledgedAt = DateTime.UtcNow.AddHours(-3), + CompletedDate = DateTime.UtcNow + }); + await context.SaveChangesAsync(); + var controller = NewController(context); + + var csv = (FileContentResult)await controller.ExportInsightsCsv(null, null, null, null, null); + var pdf = (FileContentResult)await controller.ExportInsightsPdf(null, null, null, null, null); + + csv.ContentType.Should().Be("text/csv"); + var csvText = System.Text.Encoding.UTF8.GetString(csv.FileContents); + csvText.Should().Contain("AverageCycleHours"); + csvText.Should().Contain("\"'=HYPERLINK(\"\"https://invalid.test\"\")\""); + pdf.ContentType.Should().Be("application/pdf"); + System.Text.Encoding.ASCII.GetString(pdf.FileContents, 0, 8).Should().StartWith("%PDF-1.4"); + } + + [Fact] + public async Task AssignVendor_RejectsInactiveVendor() + { + using var context = NewContext(); + var vendor = new Vendor { CompanyName = "Inactive", IsActive = false }; + var workOrder = new WorkOrder { WorkerOrderTitle = "Repair" }; + context.AddRange(vendor, workOrder); + await context.SaveChangesAsync(); + + var result = await NewController(context).AssignVendor( + workOrder.Id, + new VendorOperationsController.VendorAssignmentRequest { VendorId = vendor.Id }); + + result.Should().BeOfType(); + context.Dispatches.Should().BeEmpty(); + } + + [Fact] + public async Task AssignVendor_PersistsEtaAuditAndDisclosesConflicts() + { + using var context = NewContext(); + var start = DateTime.UtcNow.AddHours(2); + var vendor = new Vendor + { + CompanyName = "Gateway", + IsActive = true, + AvailabilityStatus = "Available", + AvailabilityUpdatedAt = DateTime.UtcNow + }; + var existingWorkOrder = new WorkOrder + { + InternalWONumber = "WO-EXISTING", + ScheduledStart = start, + ScheduledEnd = start.AddHours(2) + }; + var assignedWorkOrder = new WorkOrder + { + InternalWONumber = "WO-NEW", + ScheduledStart = start.AddMinutes(30), + ScheduledEnd = start.AddHours(1) + }; + context.AddRange(vendor, existingWorkOrder, assignedWorkOrder); + await context.SaveChangesAsync(); + context.Dispatches.Add(new Dispatch + { + VendorId = vendor.Id, + WorkOrderId = existingWorkOrder.Id, + Status = "Sent", + ScheduledDate = existingWorkOrder.ScheduledStart + }); + await context.SaveChangesAsync(); + var eta = start.AddMinutes(45); + + var result = await NewController(context).AssignVendor( + assignedWorkOrder.Id, + new VendorOperationsController.VendorAssignmentRequest + { + VendorId = vendor.Id, + ScheduledStart = assignedWorkOrder.ScheduledStart, + ScheduledEnd = assignedWorkOrder.ScheduledEnd, + EstimatedArrivalAt = eta, + EtaManualOverride = true + }); + + var value = ((OkObjectResult)result).Value!; + Prop(value, "AvailabilityStatus").Should().Be("Unavailable"); + ((IEnumerable)Prop(value, "Conflicts")).Cast().Should().ContainSingle(); + var dispatch = await context.Dispatches.SingleAsync(item => + item.WorkOrderId == assignedWorkOrder.Id); + dispatch.EstimatedArrivalAt.Should().Be(eta); + dispatch.EtaManualOverride.Should().BeTrue(); + dispatch.DispatchNumber.Should().StartWith("DISP-"); + context.WorkOrderAuditLogs.Should().ContainSingle(log => + log.WorkOrderId == assignedWorkOrder.Id && log.Action == "vendor_assigned"); + } + + [Fact] + public async Task AssignVendor_ReusesDispatchLinkedThroughMultiWorkOrderJoin() + { + using var context = NewContext(); + var originalVendor = new Vendor { CompanyName = "Original", IsActive = true }; + var replacementVendor = new Vendor { CompanyName = "Replacement", IsActive = true }; + var workOrder = new WorkOrder + { + InternalWONumber = "WO-LINKED", + ScheduledStart = DateTime.UtcNow.AddHours(2), + ScheduledEnd = DateTime.UtcNow.AddHours(4) + }; + context.AddRange(originalVendor, replacementVendor, workOrder); + await context.SaveChangesAsync(); + var dispatch = new Dispatch + { + VendorId = originalVendor.Id, + Status = "Sent", + DispatchNumber = "DISP-LINKED" + }; + context.Dispatches.Add(dispatch); + await context.SaveChangesAsync(); + context.DispatchWorkOrders.Add(new DispatchWorkOrder + { + DispatchId = dispatch.Id, + WorkOrderId = workOrder.Id + }); + await context.SaveChangesAsync(); + + var result = await NewController(context).AssignVendor( + workOrder.Id, + new VendorOperationsController.VendorAssignmentRequest + { + VendorId = replacementVendor.Id, + ScheduledStart = workOrder.ScheduledStart, + ScheduledEnd = workOrder.ScheduledEnd + }); + + result.Should().BeOfType(); + context.Dispatches.Should().ContainSingle(); + (await context.Dispatches.SingleAsync()).VendorId.Should().Be(replacementVendor.Id); + } +} diff --git a/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs b/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs new file mode 100644 index 0000000..e6b6167 --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs @@ -0,0 +1,134 @@ +using Api.SeaHavenIndustries.Controllers; +using Api.SeaHavenIndustries.Helper; +using Data.SeaHavenIndustries; +using FluentAssertions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Moq; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public sealed class VendorPortalDocumentTests : IDisposable +{ + private const string Token = "vendor-document-test-token"; + private readonly string _contentRoot = Path.Combine( + Path.GetTempPath(), + $"seahaven-vendor-documents-{Guid.NewGuid():N}"); + + private static ApplicationDbContext NewContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new ApplicationDbContext(options); + } + + private VendorPortalController NewController(ApplicationDbContext context) + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["VendorPortal:TokenLifetimeDays"] = "365", + ["SendGrid:ApiKey"] = "not-used" + }) + .Build(); + var environment = new Mock(); + environment.SetupGet(item => item.ContentRootPath).Returns(_contentRoot); + var controller = new VendorPortalController( + context, + new VendorPortalTokenService(context, configuration), + configuration, + new SendMessage(configuration), + environment.Object); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + }; + controller.Request.Headers["X-Vendor-Token"] = Token; + return controller; + } + + private static async Task<(Vendor Vendor, Dispatch Dispatch)> SeedDispatch( + ApplicationDbContext context) + { + var vendor = new Vendor { CompanyName = "Gateway", IsActive = true }; + var workOrder = new WorkOrder { WorkerOrderTitle = "Repair" }; + context.AddRange(vendor, workOrder); + await context.SaveChangesAsync(); + var dispatch = new Dispatch + { + VendorId = vendor.Id, + WorkOrderId = workOrder.Id, + Status = "Completed" + }; + context.Dispatches.Add(dispatch); + context.VendorAccessTokens.Add(new VendorAccessToken + { + VendorId = vendor.Id, + Token = Token, + IssuedAt = DateTime.UtcNow, + ExpiresAt = DateTime.UtcNow.AddDays(1) + }); + await context.SaveChangesAsync(); + return (vendor, dispatch); + } + + private static FormFile FormFile(byte[] bytes, string fileName, string contentType) + { + return new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", fileName) + { + Headers = new HeaderDictionary(), + ContentType = contentType + }; + } + + [Fact] + public async Task UploadCompletionDocument_RejectsMismatchedFileSignature() + { + using var context = NewContext(); + var (_, dispatch) = await SeedDispatch(context); + + var result = await NewController(context).UploadCompletionDocument( + dispatch.Id, + FormFile("not a pdf"u8.ToArray(), "completion.pdf", "application/pdf")); + + result.Should().BeOfType(); + context.VendorCompletionDocuments.Should().BeEmpty(); + } + + [Fact] + public async Task UploadCompletionDocument_QuarantinesValidFileUntilScanPasses() + { + using var context = NewContext(); + var (_, dispatch) = await SeedDispatch(context); + var controller = NewController(context); + var pdf = "%PDF-1.4\nvendor completion"u8.ToArray(); + + var upload = await controller.UploadCompletionDocument( + dispatch.Id, + FormFile(pdf, "completion.pdf", "application/pdf")); + + upload.Should().BeOfType(); + var document = await context.VendorCompletionDocuments.SingleAsync(); + document.ScanStatus.Should().Be("Pending"); + document.ReviewStatus.Should().Be("Processing"); + File.Exists(VendorDocumentStorage.ResolvePath(_contentRoot, document)).Should().BeTrue(); + + var download = await controller.DownloadCompletionDocument(dispatch.Id, document.Id); + + var locked = download.Should().BeOfType().Subject; + locked.StatusCode.Should().Be(StatusCodes.Status423Locked); + } + + public void Dispose() + { + if (Directory.Exists(_contentRoot)) + { + Directory.Delete(_contentRoot, recursive: true); + } + } +} diff --git a/Api.SeaHavenIndustries/Controllers/VendorOperationsController.cs b/Api.SeaHavenIndustries/Controllers/VendorOperationsController.cs new file mode 100644 index 0000000..5dd93cc --- /dev/null +++ b/Api.SeaHavenIndustries/Controllers/VendorOperationsController.cs @@ -0,0 +1,724 @@ +using System.Security.Claims; +using System.Text; +using Data.SeaHavenIndustries; +using Data.SeaHavenIndustries.Enums; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Interfaces; + +namespace Api.SeaHavenIndustries.Controllers +{ + [Authorize] + [ApiController] + [Route("api/vendor-operations")] + public class VendorOperationsController : ControllerBase + { + private static readonly string[] TerminalDispatchStatuses = { "Completed", "Verified", "Cancelled", "Canceled" }; + private readonly ApplicationDbContext _db; + private readonly IDispatchDataService _dispatchDataService; + private readonly TimeProvider _timeProvider; + + public VendorOperationsController( + ApplicationDbContext db, + IDispatchDataService dispatchDataService, + TimeProvider timeProvider) + { + _db = db; + _dispatchDataService = dispatchDataService; + _timeProvider = timeProvider; + } + + [HttpGet("notifications")] + public async Task GetNotifications() + { + var now = _timeProvider.GetUtcNow().UtcDateTime; + var cutoff = now.AddHours(48); + + var directlyAssignedIds = await _db.Dispatches + .AsNoTracking() + .Where(d => !TerminalDispatchStatuses.Contains(d.Status ?? "") && d.WorkOrderId.HasValue) + .Select(d => d.WorkOrderId!.Value) + .ToListAsync(); + var multiplyAssignedIds = await _db.DispatchWorkOrders + .AsNoTracking() + .Where(link => link.Dispatch != null + && !TerminalDispatchStatuses.Contains(link.Dispatch.Status ?? "")) + .Select(link => link.WorkOrderId) + .ToListAsync(); + var assignedWorkOrderIds = directlyAssignedIds.Concat(multiplyAssignedIds).Distinct().ToList(); + + var noVendor = await _db.workOrders + .AsNoTracking() + .Where(wo => wo.ScheduledDate.HasValue + && wo.ScheduledDate.Value <= cutoff + && wo.LifecycleStatus != LifecycleStatus.Completed + && wo.LifecycleStatus != LifecycleStatus.Canceled + && !assignedWorkOrderIds.Contains(wo.Id)) + .OrderBy(wo => wo.ScheduledDate) + .Select(wo => new + { + Id = $"no-vendor-{wo.Id}", + Type = "NoVendor", + Severity = "Medium", + Group = "No Vendor", + Title = "No vendor assigned — 48h to service", + WorkOrderId = wo.Id, + WorkOrderNumber = wo.InternalWONumber ?? wo.WorkerOrderNumber, + TriggeredAt = wo.ScheduledDate!.Value.AddHours(-48), + ScheduledAt = wo.ScheduledDate, + Action = new { Label = "Choose vendor", Href = $"/workorders/{wo.Id}?assignVendor=1" } + }) + .ToListAsync(); + + var dispatches = await ActiveScheduledDispatches().ToListAsync(); + var conflicts = BuildConflictPairs(dispatches) + .Select(pair => new + { + Id = $"vendor-conflict-{pair.Left.Id}-{pair.Right.Id}", + Type = "VendorConflict", + Severity = "Medium", + Group = "Vendor Conflict", + Title = "Vendor has overlapping appointments", + VendorId = pair.Left.VendorId, + VendorName = pair.Left.VendorName, + WorkOrders = new[] + { + new { pair.Left.WorkOrderId, pair.Left.WorkOrderNumber, pair.Left.Start, pair.Left.End }, + new { pair.Right.WorkOrderId, pair.Right.WorkOrderNumber, pair.Right.Start, pair.Right.End } + }, + TriggeredAt = pair.Left.Start < pair.Right.Start ? pair.Left.Start : pair.Right.Start, + Actions = new[] + { + new { Label = pair.Left.WorkOrderNumber ?? "Open work order", Href = $"/workorders/{pair.Left.WorkOrderId}" }, + new { Label = pair.Right.WorkOrderNumber ?? "Open work order", Href = $"/workorders/{pair.Right.WorkOrderId}" } + } + }) + .ToList(); + + return Ok(new + { + GeneratedAt = now, + Items = noVendor.Cast().Concat(conflicts).OrderBy(item => + item.GetType().GetProperty("TriggeredAt")?.GetValue(item)).ToList() + }); + } + + [HttpGet("availability")] + public async Task GetAvailability( + [FromQuery] DateTime? start, + [FromQuery] DateTime? end, + [FromQuery] int? locationId, + [FromQuery] string? trade) + { + var now = _timeProvider.GetUtcNow().UtcDateTime; + var requestedStart = start ?? now; + var requestedEnd = end ?? requestedStart.AddHours(2); + if (requestedEnd <= requestedStart) + return BadRequest(new Response { Status = "Error", Message = "end must be after start" }); + + var vendors = await _db.Vendors.AsNoTracking() + .Where(v => v.IsActive && (v.IsDeleted == null || v.IsDeleted == false)) + .ToListAsync(); + + var preferences = locationId.HasValue + ? await _db.SitePreferredVendors.AsNoTracking() + .Where(p => p.LocationId == locationId.Value) + .ToListAsync() + : new List(); + + var dispatches = await ActiveScheduledDispatches().ToListAsync(); + var result = vendors.Select(vendor => + { + var matchingDispatches = dispatches + .Where(dispatch => dispatch.VendorId == vendor.Id + && WindowsOverlap(requestedStart, requestedEnd, dispatch.Start, dispatch.End)) + .ToList(); + var matchingPreference = preferences + .Where(preference => preference.VendorId == vendor.Id + && (string.IsNullOrWhiteSpace(preference.Trade) + || string.Equals(preference.Trade, trade, StringComparison.OrdinalIgnoreCase))) + .OrderBy(preference => preference.SortOrder) + .FirstOrDefault(); + var signalIsFresh = vendor.AvailabilityUpdatedAt.HasValue + && vendor.AvailabilityUpdatedAt.Value >= now.AddHours(-24); + var status = matchingDispatches.Count > 0 + ? "Unavailable" + : signalIsFresh && new[] { "Available", "Unavailable" }.Contains(vendor.AvailabilityStatus) + ? vendor.AvailabilityStatus + : "Unknown"; + + return new + { + vendor.Id, + vendor.CompanyName, + vendor.ContactName, + vendor.TradeSpecialties, + AvailabilityStatus = status, + vendor.AvailabilityUpdatedAt, + IsPreferred = matchingPreference != null, + PreferenceRank = matchingPreference?.SortOrder, + Conflicts = matchingDispatches.Select(dispatch => new + { + dispatch.WorkOrderId, + dispatch.WorkOrderNumber, + dispatch.Start, + dispatch.End + }) + }; + }) + .OrderBy(vendor => vendor.IsPreferred ? 0 : 1) + .ThenBy(vendor => vendor.PreferenceRank ?? int.MaxValue) + .ThenBy(vendor => vendor.AvailabilityStatus == "Available" ? 0 + : vendor.AvailabilityStatus == "Unknown" ? 1 : 2) + .ThenBy(vendor => vendor.CompanyName) + .ToList(); + + return Ok(new { GeneratedAt = now, StaleAfterHours = 24, Data = result }); + } + + [HttpPut("vendors/{vendorId:int}/availability")] + public async Task UpdateAvailability(int vendorId, [FromBody] AvailabilityRequest body) + { + var normalized = body.Status?.Trim(); + if (normalized is not ("Available" or "Unavailable" or "Unknown")) + return BadRequest(new Response { Status = "Error", Message = "status must be Available, Unavailable, or Unknown" }); + + var vendor = await _db.Vendors.FindAsync(vendorId); + if (vendor == null) return NotFound(); + + var now = _timeProvider.GetUtcNow().UtcDateTime; + var actor = Actor(); + var previous = vendor.AvailabilityStatus; + vendor.AvailabilityStatus = normalized; + vendor.AvailabilityUpdatedAt = now; + vendor.LastModificationTime = now; + _db.VendorAuditLogs.Add(new VendorAuditLog + { + VendorId = vendorId, + FieldName = nameof(Vendor.AvailabilityStatus), + OldValue = previous, + NewValue = normalized, + Actor = actor, + CreatedAt = now + }); + await _db.SaveChangesAsync(); + return Ok(new { vendor.Id, vendor.AvailabilityStatus, vendor.AvailabilityUpdatedAt }); + } + + [HttpGet("sites/{locationId:int}/preferred-vendors")] + public async Task GetSitePreferences(int locationId) + { + var preferences = await _db.SitePreferredVendors + .AsNoTracking() + .Where(p => p.LocationId == locationId) + .Include(p => p.Vendor) + .OrderBy(p => p.SortOrder) + .Select(p => new + { + p.Id, + p.LocationId, + p.VendorId, + VendorName = p.Vendor!.CompanyName, + p.Trade, + p.SortOrder, + IsStale = !p.Vendor.IsActive || (p.Vendor.IsDeleted ?? false), + p.LastModificationTime, + p.UpdatedBy + }) + .ToListAsync(); + return Ok(preferences); + } + + [HttpPut("sites/{locationId:int}/preferred-vendors")] + public async Task ReplaceSitePreferences(int locationId, [FromBody] List body) + { + if (!await _db.Locations.AnyAsync(location => location.Id == locationId)) + return NotFound(new Response { Status = "Error", Message = "Location not found" }); + if (body.GroupBy(item => new { item.VendorId, Trade = item.Trade?.Trim().ToLowerInvariant() }).Any(group => group.Count() > 1)) + return BadRequest(new Response { Status = "Error", Message = "Duplicate vendor/trade preference" }); + + var vendorIds = body.Select(item => item.VendorId).Distinct().ToList(); + var vendors = await _db.Vendors.Where(vendor => vendorIds.Contains(vendor.Id)).ToDictionaryAsync(vendor => vendor.Id); + foreach (var item in body) + { + if (!vendors.TryGetValue(item.VendorId, out var vendor) || !vendor.IsActive || (vendor.IsDeleted ?? false)) + return BadRequest(new Response { Status = "Error", Message = $"Vendor {item.VendorId} is inactive or missing" }); + if (!string.IsNullOrWhiteSpace(item.Trade) + && !(vendor.TradeSpecialties ?? "").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .Contains(item.Trade.Trim(), StringComparer.OrdinalIgnoreCase)) + return BadRequest(new Response { Status = "Error", Message = $"Vendor {item.VendorId} does not support trade '{item.Trade}'" }); + } + + var now = _timeProvider.GetUtcNow().UtcDateTime; + var actor = Actor(); + var existing = await _db.SitePreferredVendors.Where(p => p.LocationId == locationId).ToListAsync(); + _db.SitePreferredVendors.RemoveRange(existing); + _db.SitePreferredVendors.AddRange(body.Select((item, index) => new SitePreferredVendor + { + LocationId = locationId, + VendorId = item.VendorId, + Trade = string.IsNullOrWhiteSpace(item.Trade) ? null : item.Trade.Trim(), + SortOrder = item.SortOrder ?? index, + CreatedDate = now, + LastModificationTime = now, + createdby = actor, + UpdatedBy = actor + })); + foreach (var vendorId in existing.Select(p => p.VendorId).Concat(vendorIds).Distinct()) + { + _db.VendorAuditLogs.Add(new VendorAuditLog + { + VendorId = vendorId, + FieldName = $"SitePreference:{locationId}", + OldValue = string.Join(",", existing.Where(p => p.VendorId == vendorId).Select(p => $"{p.Trade}:{p.SortOrder}")), + NewValue = string.Join(",", body.Where(p => p.VendorId == vendorId).Select(p => $"{p.Trade}:{p.SortOrder}")), + Actor = actor, + CreatedAt = now + }); + } + await _db.SaveChangesAsync(); + return await GetSitePreferences(locationId); + } + + [HttpPut("documents/{documentId:int}/review")] + public async Task ReviewDocument(int documentId, [FromBody] DocumentReviewRequest body) + { + if (body.ReviewStatus is not ("Approved" or "Rejected")) + return BadRequest(new Response { Status = "Error", Message = "reviewStatus must be Approved or Rejected" }); + if (body.ReviewStatus == "Rejected" && string.IsNullOrWhiteSpace(body.RejectionReason)) + return BadRequest(new Response { Status = "Error", Message = "A rejection reason is required" }); + + var document = await _db.VendorCompletionDocuments.FindAsync(documentId); + if (document == null) return NotFound(); + if (document.ScanStatus != "Passed") + return Conflict(new Response { Status = "Conflict", Message = "A document cannot be approved or reviewed until malware scanning passes" }); + + var now = _timeProvider.GetUtcNow().UtcDateTime; + var previous = document.ReviewStatus; + document.ReviewStatus = body.ReviewStatus; + document.RejectionReason = body.ReviewStatus == "Rejected" ? body.RejectionReason?.Trim() : null; + document.ReviewedAt = now; + document.LastModificationTime = now; + _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog + { + WorkOrderId = document.WorkOrderId, + UserId = Actor(), + FieldName = $"Completion Document {document.OriginalFileName}", + OldValue = previous, + NewValue = document.ReviewStatus, + Action = "completion_document_reviewed", + CreatedAt = now + }); + await _db.SaveChangesAsync(); + return Ok(new { document.Id, document.ReviewStatus, document.RejectionReason, document.ReviewedAt }); + } + + [HttpPut("dispatches/{dispatchId:int}/commercial-status")] + public async Task UpdateCommercialStatus(int dispatchId, [FromBody] CommercialStatusRequest body) + { + var invoiceStatuses = new[] { "Not Submitted", "Processing", "Confirmed", "Rejected" }; + var paymentStatuses = new[] { "Unavailable", "Pending", "Scheduled", "Paid", "On Hold" }; + if (!invoiceStatuses.Contains(body.InvoiceStatus)) + return BadRequest(new Response { Status = "Error", Message = "Invalid invoice status" }); + if (!paymentStatuses.Contains(body.PaymentStatus)) + return BadRequest(new Response { Status = "Error", Message = "Invalid payment status" }); + + var dispatch = await _db.Dispatches.FindAsync(dispatchId); + if (dispatch == null) return NotFound(); + var now = _timeProvider.GetUtcNow().UtcDateTime; + var oldValue = $"{dispatch.InvoiceStatus}|{dispatch.PaymentStatus}|{dispatch.InvoiceNumber}"; + dispatch.InvoiceStatus = body.InvoiceStatus; + dispatch.PaymentStatus = body.PaymentStatus; + dispatch.InvoiceNumber = string.IsNullOrWhiteSpace(body.InvoiceNumber) ? null : body.InvoiceNumber.Trim(); + dispatch.CommercialStatusUpdatedAt = now; + dispatch.LastModificationTime = now; + _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog + { + WorkOrderId = dispatch.WorkOrderId ?? 0, + UserId = Actor(), + FieldName = $"Dispatch {dispatch.DispatchNumber} Commercial Status", + OldValue = oldValue, + NewValue = $"{dispatch.InvoiceStatus}|{dispatch.PaymentStatus}|{dispatch.InvoiceNumber}", + Action = "commercial_status_updated", + CreatedAt = now + }); + await _db.SaveChangesAsync(); + return Ok(new + { + dispatch.Id, + dispatch.PONumber, + dispatch.InvoiceNumber, + dispatch.InvoiceStatus, + dispatch.PaymentStatus, + dispatch.CommercialStatusUpdatedAt + }); + } + + [HttpPut("work-orders/{workOrderId:int}/assignment")] + public async Task AssignVendor(int workOrderId, [FromBody] VendorAssignmentRequest body) + { + var workOrder = await _db.workOrders.FindAsync(workOrderId); + if (workOrder == null) return NotFound(new Response { Status = "Error", Message = "Work order not found" }); + var vendor = await _db.Vendors.FindAsync(body.VendorId); + if (vendor == null || !vendor.IsActive || (vendor.IsDeleted ?? false)) + return BadRequest(new Response { Status = "Error", Message = "Only an active vendor can be assigned" }); + + var start = body.ScheduledStart ?? workOrder.ScheduledStart ?? workOrder.ScheduledDate; + var end = body.ScheduledEnd ?? workOrder.ScheduledEnd ?? start?.AddHours(2); + var scheduledDispatches = start.HasValue && end.HasValue + ? await ActiveScheduledDispatches() + .Where(dispatch => dispatch.VendorId == vendor.Id && dispatch.WorkOrderId != workOrderId) + .ToListAsync() + : new List(); + var conflicts = start.HasValue && end.HasValue + ? scheduledDispatches.Where(dispatch => WindowsOverlap(start.Value, end.Value, dispatch.Start, dispatch.End)).ToList() + : new List(); + + var linkedDispatchIds = _db.DispatchWorkOrders + .Where(link => link.WorkOrderId == workOrderId) + .Select(link => link.DispatchId); + var dispatch = await _db.Dispatches + .Where(item => (item.WorkOrderId == workOrderId || linkedDispatchIds.Contains(item.Id)) + && !TerminalDispatchStatuses.Contains(item.Status ?? "")) + .OrderByDescending(item => item.CreatedDate) + .FirstOrDefaultAsync(); + var now = _timeProvider.GetUtcNow().UtcDateTime; + var previousVendorId = dispatch?.VendorId; + if (dispatch == null) + { + dispatch = new Dispatch + { + WorkOrderId = workOrderId, + VendorId = vendor.Id, + DispatchNumber = await _dispatchDataService.GenerateNextDispatchNumberAsync(), + PONumber = await _dispatchDataService.GenerateNextPONumberAsync(), + Status = "Sent", + ScheduledDate = start, + EstimatedArrivalAt = body.EstimatedArrivalAt, + EtaManualOverride = body.EtaManualOverride, + DispatchedAt = now, + CreatedDate = now, + createdby = Actor() + }; + _db.Dispatches.Add(dispatch); + } + else + { + dispatch.VendorId = vendor.Id; + dispatch.ScheduledDate = start; + dispatch.EstimatedArrivalAt = body.EstimatedArrivalAt; + dispatch.EtaManualOverride = body.EtaManualOverride; + dispatch.LastModificationTime = now; + } + + workOrder.ScheduledStart = body.ScheduledStart ?? workOrder.ScheduledStart; + workOrder.ScheduledEnd = body.ScheduledEnd ?? workOrder.ScheduledEnd; + workOrder.ScheduledDate = start ?? workOrder.ScheduledDate; + workOrder.LastModificationTime = now; + _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog + { + WorkOrderId = workOrderId, + UserId = Actor(), + FieldName = "Vendor Assignment", + OldValue = previousVendorId?.ToString(), + NewValue = vendor.Id.ToString(), + Action = previousVendorId.HasValue ? "vendor_assignment_changed" : "vendor_assigned", + CreatedAt = now + }); + await _db.SaveChangesAsync(); + + return Ok(new + { + dispatch.Id, + dispatch.VendorId, + VendorName = vendor.CompanyName, + dispatch.EstimatedArrivalAt, + dispatch.EtaManualOverride, + AvailabilityStatus = conflicts.Count > 0 ? "Unavailable" + : vendor.AvailabilityUpdatedAt >= now.AddHours(-24) ? vendor.AvailabilityStatus : "Unknown", + Conflicts = conflicts.Select(conflict => new + { + conflict.WorkOrderId, + conflict.WorkOrderNumber, + conflict.Start, + conflict.End + }) + }); + } + + [HttpGet("insights")] + public async Task GetInsights( + [FromQuery] DateTime? from, + [FromQuery] DateTime? to, + [FromQuery] int? locationId, + [FromQuery] string? trade, + [FromQuery] int? vendorId) + { + var rows = await BuildInsightRows(from, to, locationId, trade, vendorId); + return Ok(new + { + GeneratedAt = _timeProvider.GetUtcNow().UtcDateTime, + Filters = new { from, to, locationId, trade, vendorId }, + MetricDefinitions = MetricDefinitions(), + Data = rows + }); + } + + [HttpGet("insights.csv")] + public async Task ExportInsightsCsv( + [FromQuery] DateTime? from, + [FromQuery] DateTime? to, + [FromQuery] int? locationId, + [FromQuery] string? trade, + [FromQuery] int? vendorId) + { + var rows = await BuildInsightRows(from, to, locationId, trade, vendorId); + var csv = new StringBuilder("VendorId,Vendor,CompletedJobs,CancellationRefusalRate,OnTimeArrivalRate,OnTimeCompletionRate,AverageCycleHours\n"); + foreach (var row in rows) + { + csv.AppendLine(string.Join(",", + row.VendorId, + Csv(row.Vendor), + row.CompletedJobs, + row.CancellationRefusalRate.ToString("0.####"), + row.OnTimeArrivalRate.ToString("0.####"), + row.OnTimeCompletionRate.ToString("0.####"), + row.AverageCycleHours?.ToString("0.##") ?? "")); + } + return File(Encoding.UTF8.GetBytes(csv.ToString()), "text/csv", $"vendor-insights-{DateTime.UtcNow:yyyyMMddHHmmss}.csv"); + } + + [HttpGet("insights.pdf")] + public async Task ExportInsightsPdf( + [FromQuery] DateTime? from, + [FromQuery] DateTime? to, + [FromQuery] int? locationId, + [FromQuery] string? trade, + [FromQuery] int? vendorId) + { + var rows = await BuildInsightRows(from, to, locationId, trade, vendorId); + var lines = new List + { + "Vendor Performance Insights", + $"Generated: {_timeProvider.GetUtcNow():u}", + $"Filters: from={from:u}; to={to:u}; site={locationId}; trade={trade}; vendor={vendorId}", + "Definitions: completed jobs; cancellation/refusal rate; on-time acknowledgement; on-time completion; average dispatch-to-completion hours." + }; + lines.AddRange(rows.Take(35).Select(row => + $"{row.Vendor}: completed {row.CompletedJobs}; cancel/refusal {row.CancellationRefusalRate:P1}; arrival {row.OnTimeArrivalRate:P1}; completion {row.OnTimeCompletionRate:P1}; cycle {row.AverageCycleHours:0.0}h")); + return File(SimplePdf(lines), "application/pdf", $"vendor-insights-{DateTime.UtcNow:yyyyMMddHHmmss}.pdf"); + } + + private IQueryable ActiveScheduledDispatches() + { + var direct = _db.Dispatches.AsNoTracking() + .Where(dispatch => !TerminalDispatchStatuses.Contains(dispatch.Status ?? "") + && (dispatch.IsDeleted == null || dispatch.IsDeleted == false) + && dispatch.WorkOrderId.HasValue + && (dispatch.ScheduledDate.HasValue + || (dispatch.WorkOrder != null && dispatch.WorkOrder.ScheduledStart.HasValue))) + .Select(dispatch => new ScheduledDispatch + { + Id = dispatch.Id, + VendorId = dispatch.VendorId, + VendorName = dispatch.Vendor!.CompanyName, + WorkOrderId = dispatch.WorkOrderId!.Value, + WorkOrderNumber = dispatch.WorkOrder!.InternalWONumber ?? dispatch.WorkOrder.WorkerOrderNumber, + Start = dispatch.WorkOrder!.ScheduledStart ?? dispatch.ScheduledDate!.Value, + End = dispatch.WorkOrder.ScheduledEnd + ?? (dispatch.WorkOrder.ScheduledStart ?? dispatch.ScheduledDate!.Value).AddHours(2) + }); + + var linked = _db.DispatchWorkOrders.AsNoTracking() + .Where(link => link.Dispatch != null + && link.WorkOrder != null + && link.Dispatch.WorkOrderId != link.WorkOrderId + && !TerminalDispatchStatuses.Contains(link.Dispatch.Status ?? "") + && (link.Dispatch.IsDeleted == null || link.Dispatch.IsDeleted == false) + && (link.Dispatch.ScheduledDate.HasValue || link.WorkOrder.ScheduledStart.HasValue)) + .Select(link => new ScheduledDispatch + { + Id = link.DispatchId, + VendorId = link.Dispatch!.VendorId, + VendorName = link.Dispatch.Vendor!.CompanyName, + WorkOrderId = link.WorkOrderId, + WorkOrderNumber = link.WorkOrder!.InternalWONumber ?? link.WorkOrder.WorkerOrderNumber, + Start = link.WorkOrder.ScheduledStart ?? link.Dispatch.ScheduledDate!.Value, + End = link.WorkOrder.ScheduledEnd + ?? (link.WorkOrder.ScheduledStart ?? link.Dispatch.ScheduledDate!.Value).AddHours(2) + }); + + return direct.Concat(linked); + } + + private static IEnumerable<(ScheduledDispatch Left, ScheduledDispatch Right)> BuildConflictPairs(List dispatches) + { + for (var leftIndex = 0; leftIndex < dispatches.Count; leftIndex++) + { + for (var rightIndex = leftIndex + 1; rightIndex < dispatches.Count; rightIndex++) + { + var left = dispatches[leftIndex]; + var right = dispatches[rightIndex]; + if (left.VendorId == right.VendorId + && left.WorkOrderId != right.WorkOrderId + && WindowsOverlap(left.Start, left.End, right.Start, right.End)) + yield return (left, right); + } + } + } + + private async Task> BuildInsightRows( + DateTime? from, DateTime? to, int? locationId, string? trade, int? vendorId) + { + var query = _db.Dispatches.AsNoTracking() + .Include(dispatch => dispatch.Vendor) + .Include(dispatch => dispatch.WorkOrder) + .Where(dispatch => dispatch.Vendor != null); + if (from.HasValue) query = query.Where(dispatch => (dispatch.DispatchedAt ?? dispatch.CreatedDate) >= from); + if (to.HasValue) query = query.Where(dispatch => (dispatch.DispatchedAt ?? dispatch.CreatedDate) <= to); + if (locationId.HasValue) query = query.Where(dispatch => dispatch.WorkOrder!.LocationId == locationId); + if (!string.IsNullOrWhiteSpace(trade)) query = query.Where(dispatch => dispatch.WorkOrder!.Trade == trade); + if (vendorId.HasValue) query = query.Where(dispatch => dispatch.VendorId == vendorId); + + var dispatches = await query.ToListAsync(); + return dispatches.GroupBy(dispatch => dispatch.VendorId) + .Select(group => + { + var completed = group.Where(dispatch => dispatch.Status is "Completed" or "Verified").ToList(); + var canceled = group.Count(dispatch => dispatch.Status is "Cancelled" or "Canceled" or "Refused"); + var acknowledgedWithSchedule = group.Where(dispatch => dispatch.AcknowledgedAt.HasValue && dispatch.ScheduledDate.HasValue).ToList(); + var completedWithDue = completed.Where(dispatch => dispatch.CompletedDate.HasValue && dispatch.WorkOrder?.DueDate != null).ToList(); + var cycleRows = completed.Where(dispatch => dispatch.CompletedDate.HasValue && (dispatch.DispatchedAt ?? dispatch.CreatedDate).HasValue).ToList(); + return new VendorInsightRow + { + VendorId = group.Key, + Vendor = group.First().Vendor?.CompanyName ?? $"Vendor {group.Key}", + CompletedJobs = completed.Count, + CancellationRefusalRate = Rate(canceled, group.Count()), + OnTimeArrivalRate = Rate( + acknowledgedWithSchedule.Count(dispatch => dispatch.AcknowledgedAt <= dispatch.ScheduledDate), + acknowledgedWithSchedule.Count), + OnTimeCompletionRate = Rate( + completedWithDue.Count(dispatch => dispatch.CompletedDate <= dispatch.WorkOrder!.DueDate), + completedWithDue.Count), + AverageCycleHours = cycleRows.Count == 0 ? null : cycleRows.Average(dispatch => + (dispatch.CompletedDate!.Value - (dispatch.DispatchedAt ?? dispatch.CreatedDate)!.Value).TotalHours) + }; + }) + .OrderBy(row => row.Vendor) + .ToList(); + } + + private string Actor() => User.FindFirstValue(ClaimTypes.NameIdentifier) + ?? User.Identity?.Name + ?? "unknown"; + + private static bool WindowsOverlap(DateTime leftStart, DateTime leftEnd, DateTime rightStart, DateTime rightEnd) + => leftStart < rightEnd && rightStart < leftEnd; + + private static decimal Rate(int numerator, int denominator) + => denominator == 0 ? 0 : Math.Round((decimal)numerator / denominator, 4); + + private static string Csv(string value) + { + var spreadsheetSafe = value.Length > 0 && "=+-@\t\r".Contains(value[0]) + ? $"'{value}" + : value; + return $"\"{spreadsheetSafe.Replace("\"", "\"\"")}\""; + } + + private static object MetricDefinitions() => new + { + CompletedJobs = "Dispatches with Completed or Verified status.", + CancellationRefusalRate = "Cancelled, Canceled, or Refused dispatches divided by all dispatches.", + OnTimeArrivalRate = "Acknowledgements at or before scheduled time divided by dispatches with both timestamps.", + OnTimeCompletionRate = "Completions at or before the work-order due date divided by completed dispatches with a due date.", + AverageCycleHours = "Average elapsed hours from dispatch to completion." + }; + + private static byte[] SimplePdf(IEnumerable lines) + { + var escaped = lines.Select(line => line.Replace("\\", "\\\\").Replace("(", "\\(").Replace(")", "\\)")); + var content = "BT /F1 10 Tf 40 760 Td 13 TL " + + string.Join(" T* ", escaped.Select(line => $"({line}) Tj")) + + " ET"; + var objects = new[] + { + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>", + $"<< /Length {Encoding.ASCII.GetByteCount(content)} >>\nstream\n{content}\nendstream", + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>" + }; + var builder = new StringBuilder("%PDF-1.4\n"); + var offsets = new List { 0 }; + for (var index = 0; index < objects.Length; index++) + { + offsets.Add(Encoding.ASCII.GetByteCount(builder.ToString())); + builder.Append($"{index + 1} 0 obj\n{objects[index]}\nendobj\n"); + } + var xrefOffset = Encoding.ASCII.GetByteCount(builder.ToString()); + builder.Append($"xref\n0 {objects.Length + 1}\n0000000000 65535 f \n"); + foreach (var offset in offsets.Skip(1)) builder.Append($"{offset:D10} 00000 n \n"); + builder.Append($"trailer << /Size {objects.Length + 1} /Root 1 0 R >>\nstartxref\n{xrefOffset}\n%%EOF"); + return Encoding.ASCII.GetBytes(builder.ToString()); + } + + public sealed class AvailabilityRequest + { + public string? Status { get; set; } + } + + public sealed class SitePreferenceRequest + { + public int VendorId { get; set; } + public string? Trade { get; set; } + public int? SortOrder { get; set; } + } + + public sealed class DocumentReviewRequest + { + public string ReviewStatus { get; set; } = ""; + public string? RejectionReason { get; set; } + } + + public sealed class CommercialStatusRequest + { + public string InvoiceStatus { get; set; } = "Not Submitted"; + public string PaymentStatus { get; set; } = "Unavailable"; + public string? InvoiceNumber { get; set; } + } + + public sealed class VendorAssignmentRequest + { + public int VendorId { get; set; } + public DateTime? ScheduledStart { get; set; } + public DateTime? ScheduledEnd { get; set; } + public DateTime? EstimatedArrivalAt { get; set; } + public bool EtaManualOverride { get; set; } + } + + private sealed class ScheduledDispatch + { + public int Id { get; set; } + public int VendorId { get; set; } + public string? VendorName { get; set; } + public int WorkOrderId { get; set; } + public string? WorkOrderNumber { get; set; } + public DateTime Start { get; set; } + public DateTime End { get; set; } + } + + private sealed class VendorInsightRow + { + public int VendorId { get; set; } + public string Vendor { get; set; } = ""; + public int CompletedJobs { get; set; } + public decimal CancellationRefusalRate { get; set; } + public decimal OnTimeArrivalRate { get; set; } + public decimal OnTimeCompletionRate { get; set; } + public double? AverageCycleHours { get; set; } + } + } +} diff --git a/Api.SeaHavenIndustries/Controllers/VendorPortalController.cs b/Api.SeaHavenIndustries/Controllers/VendorPortalController.cs index 8841861..eba515f 100644 --- a/Api.SeaHavenIndustries/Controllers/VendorPortalController.cs +++ b/Api.SeaHavenIndustries/Controllers/VendorPortalController.cs @@ -20,13 +20,20 @@ namespace Api.SeaHavenIndustries.Controllers private readonly VendorPortalTokenService _tokens; private readonly IConfiguration _config; private readonly SendMessage _sendMessage; + private readonly IWebHostEnvironment _environment; - public VendorPortalController(ApplicationDbContext db, VendorPortalTokenService tokens, IConfiguration config, SendMessage sendMessage) + public VendorPortalController( + ApplicationDbContext db, + VendorPortalTokenService tokens, + IConfiguration config, + SendMessage sendMessage, + IWebHostEnvironment environment) { _db = db; _tokens = tokens; _config = config; _sendMessage = sendMessage; + _environment = environment; } [HttpGet("session")] @@ -71,6 +78,9 @@ namespace Api.SeaHavenIndustries.Controllers d.Id, d.DispatchNumber, d.PONumber, + d.InvoiceNumber, + d.InvoiceStatus, + d.PaymentStatus, d.Status, d.NTEAmount, d.ScheduledDate, @@ -108,6 +118,27 @@ namespace Api.SeaHavenIndustries.Controllers .Select(s => new { s.Id, s.SignoffType, s.Name, s.SignatureMethod, s.SignedAt }) .ToListAsync(); + var documents = await _db.VendorCompletionDocuments + .Where(document => document.DispatchId == id && document.VendorId == vendor.Id) + .OrderByDescending(document => document.CreatedDate) + .Select(document => new + { + document.Id, + document.OriginalFileName, + document.ContentType, + document.SizeBytes, + document.ScanStatus, + document.ReviewStatus, + document.RejectionReason, + document.Version, + document.ReplacesDocumentId, + document.CreatedDate, + document.ScannedAt, + document.ReviewedAt, + CanDownload = document.ScanStatus == "Passed" + }) + .ToListAsync(); + 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 @@ -210,6 +241,10 @@ namespace Api.SeaHavenIndustries.Controllers dispatch.Id, dispatch.DispatchNumber, dispatch.PONumber, + dispatch.InvoiceNumber, + dispatch.InvoiceStatus, + dispatch.PaymentStatus, + dispatch.CommercialStatusUpdatedAt, dispatch.Status, dispatch.NTEAmount, dispatch.Description, @@ -239,6 +274,7 @@ namespace Api.SeaHavenIndustries.Controllers }, Checklist = checklist, Signoffs = signoffs, + Documents = documents, Comments = comments, UpliftRequests = upliftRequests } @@ -589,6 +625,133 @@ namespace Api.SeaHavenIndustries.Controllers return Ok(new DataResponse { Status = "Success", Data = new { req.Id, req.Status } }); } + [HttpPost("dispatches/{id:int}/documents")] + [RequestSizeLimit(10 * 1024 * 1024)] + public async Task UploadCompletionDocument( + int id, + [FromForm] IFormFile file, + [FromForm] int? replacesDocumentId = null) + { + var vendor = await ResolveVendorAsync(); + if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" }); + var dispatch = await LoadVendorDispatchAsync(id, vendor.Id); + if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" }); + if (file == null || file.Length == 0) + return BadRequest(new Response { Status = "Error", Message = "A non-empty file is required" }); + if (file.Length > 10 * 1024 * 1024) + return BadRequest(new Response { Status = "Error", Message = "Files must be 10 MB or smaller" }); + + var allowed = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["application/pdf"] = new[] { ".pdf" }, + ["image/jpeg"] = new[] { ".jpg", ".jpeg" }, + ["image/png"] = new[] { ".png" } + }; + var extension = Path.GetExtension(file.FileName); + if (!allowed.TryGetValue(file.ContentType, out var extensions) + || !extensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + return BadRequest(new Response { Status = "Error", Message = "Only PDF, JPG, and PNG files are allowed" }); + + await using var signatureStream = file.OpenReadStream(); + if (!await HasExpectedSignature(signatureStream, file.ContentType)) + return BadRequest(new Response { Status = "Error", Message = "File content does not match its declared type" }); + + var workOrderId = dispatch.WorkOrderId + ?? await _db.DispatchWorkOrders + .Where(link => link.DispatchId == dispatch.Id) + .OrderBy(link => link.Id) + .Select(link => link.WorkOrderId) + .FirstOrDefaultAsync(); + if (workOrderId == 0) + return BadRequest(new Response { Status = "Error", Message = "Dispatch is not linked to a work order" }); + + VendorCompletionDocument? replaced = null; + if (replacesDocumentId.HasValue) + { + replaced = await _db.VendorCompletionDocuments.FirstOrDefaultAsync(document => + document.Id == replacesDocumentId.Value + && document.DispatchId == id + && document.VendorId == vendor.Id); + if (replaced == null) + return BadRequest(new Response { Status = "Error", Message = "Replacement document was not found" }); + } + + var now = DateTime.UtcNow; + var document = new VendorCompletionDocument + { + VendorId = vendor.Id, + DispatchId = id, + WorkOrderId = workOrderId, + OriginalFileName = Path.GetFileName(file.FileName), + StoredFileName = $"{Guid.NewGuid():N}{extension.ToLowerInvariant()}", + ContentType = file.ContentType, + SizeBytes = file.Length, + ScanStatus = "Pending", + ReviewStatus = "Processing", + Version = (replaced?.Version ?? 0) + 1, + ReplacesDocumentId = replaced?.Id, + CreatedDate = now, + createdby = $"vendor:{vendor.Id}" + }; + var path = VendorDocumentStorage.ResolvePath(_environment.ContentRootPath, document); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + await using (var target = System.IO.File.Create(path)) + { + await file.CopyToAsync(target); + } + + try + { + _db.VendorCompletionDocuments.Add(document); + _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog + { + WorkOrderId = document.WorkOrderId, + FieldName = "Completion Document", + OldValue = replaced?.OriginalFileName, + NewValue = document.OriginalFileName, + Action = replaced == null ? "vendor_document_uploaded" : "vendor_document_replaced", + CreatedAt = now + }); + await _db.SaveChangesAsync(); + } + catch + { + System.IO.File.Delete(path); + throw; + } + + return Ok(new DataResponse + { + Status = "Success", + Data = new + { + document.Id, + document.OriginalFileName, + document.SizeBytes, + document.ScanStatus, + document.ReviewStatus, + document.Version, + document.ReplacesDocumentId + } + }); + } + + [HttpGet("dispatches/{id:int}/documents/{documentId:int}")] + public async Task DownloadCompletionDocument(int id, int documentId) + { + var vendor = await ResolveVendorAsync(); + if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" }); + var document = await _db.VendorCompletionDocuments.AsNoTracking().FirstOrDefaultAsync(item => + item.Id == documentId && item.DispatchId == id && item.VendorId == vendor.Id); + if (document == null) return NotFound(); + if (document.ScanStatus != "Passed") + return StatusCode(StatusCodes.Status423Locked, + new Response { Status = "Processing", Message = "This file remains quarantined until malware scanning passes" }); + var path = VendorDocumentStorage.ResolvePath(_environment.ContentRootPath, document); + if (!System.IO.File.Exists(path)) return NotFound(); + return PhysicalFile(path, document.ContentType, document.OriginalFileName); + } + private async Task NotifyDispatcherOfUpliftAsync(Dispatch dispatch, DispatchUpliftRequest req, Vendor vendor) { try @@ -636,6 +799,19 @@ namespace Api.SeaHavenIndustries.Controllers } } + private static async Task HasExpectedSignature(Stream stream, string contentType) + { + var bytes = new byte[8]; + var count = await stream.ReadAsync(bytes); + if (contentType == "application/pdf") + return count >= 5 && bytes[0] == 0x25 && bytes[1] == 0x50 && bytes[2] == 0x44 && bytes[3] == 0x46 && bytes[4] == 0x2D; + if (contentType == "image/jpeg") + return count >= 3 && bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF; + if (contentType == "image/png") + return count >= 8 && bytes.SequenceEqual(new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }); + return false; + } + private async Task ResolveVendorAsync() { if (!Request.Headers.TryGetValue(TokenHeader, out var values)) return null; diff --git a/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs b/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs index 9c3b181..1f86f21 100644 --- a/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs +++ b/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs @@ -494,6 +494,8 @@ namespace Api.SeaHavenIndustries.Controllers d.Description, d.Status, d.ScheduledDate, + d.EstimatedArrivalAt, + d.EtaManualOverride, d.CompletedDate, d.DispatchedAt, d.EmailSent, @@ -863,7 +865,10 @@ namespace Api.SeaHavenIndustries.Controllers foreach (var vendorId in model.VendorIds) { var vendor = await _vendorDataService.GetByIdAsync(vendorId); - if (vendor == null) continue; + if (vendor == null) + return BadRequest(new Response { Status = "Error", Message = $"Vendor {vendorId} was not found" }); + if (!vendor.IsActive || (vendor.IsDeleted ?? false)) + return BadRequest(new Response { Status = "Error", Message = $"Vendor {vendorId} is inactive and cannot receive a new assignment" }); var poNumber = $"VPO-{nextPO:D5}"; nextPO++; @@ -882,6 +887,8 @@ namespace Api.SeaHavenIndustries.Controllers Description = model.Description, Status = "Sent", ScheduledDate = model.ScheduledDate, + EstimatedArrivalAt = model.EstimatedArrivalAt, + EtaManualOverride = model.EtaManualOverride, DispatchedAt = DateTime.UtcNow, ReplyToAddress = replyTo, DispatchWorkOrders = workOrders.Select(wo => new DispatchWorkOrder { WorkOrderId = wo.Id }).ToList() @@ -961,6 +968,19 @@ namespace Api.SeaHavenIndustries.Controllers Action = AuditAction.Dispatch.ToString(), CreatedAt = DateTime.UtcNow }); + if (model.EstimatedArrivalAt.HasValue) + { + await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog + { + WorkOrderId = wo.Id, + UserId = userId, + FieldName = "Vendor ETA", + OldValue = "", + NewValue = model.EstimatedArrivalAt.Value.ToUniversalTime().ToString("O"), + Action = model.EtaManualOverride ? "vendor_eta_manual_override" : "vendor_eta_set", + CreatedAt = DateTime.UtcNow + }); + } } dispatches.Add(new { savedDispatch.Id, dispatchNumber, poNumber, vendorName = vendor.CompanyName, savedDispatch.Status, workOrderCount = workOrders.Count }); @@ -987,6 +1007,8 @@ namespace Api.SeaHavenIndustries.Controllers d.Description, d.Status, d.ScheduledDate, + d.EstimatedArrivalAt, + d.EtaManualOverride, d.CompletedDate, d.DispatchedAt, d.EmailSent, @@ -1013,6 +1035,8 @@ namespace Api.SeaHavenIndustries.Controllers x.Description, x.Status, x.ScheduledDate, + x.EstimatedArrivalAt, + x.EtaManualOverride, x.CompletedDate, x.DispatchedAt, x.EmailSent, @@ -1101,6 +1125,21 @@ namespace Api.SeaHavenIndustries.Controllers dispatch.Status = model.Status; dispatch.NTEAmount = model.NTEAmount; dispatch.ScheduledDate = model.ScheduledDate; + if (dispatch.EstimatedArrivalAt != model.EstimatedArrivalAt) + { + _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog + { + WorkOrderId = dispatch.WorkOrderId ?? 0, + UserId = userId, + FieldName = "Vendor ETA", + OldValue = dispatch.EstimatedArrivalAt?.ToUniversalTime().ToString("O") ?? "", + NewValue = model.EstimatedArrivalAt?.ToUniversalTime().ToString("O") ?? "", + Action = model.EtaManualOverride ? "vendor_eta_manual_override" : "vendor_eta_change", + CreatedAt = DateTime.UtcNow + }); + } + dispatch.EstimatedArrivalAt = model.EstimatedArrivalAt; + dispatch.EtaManualOverride = model.EtaManualOverride; dispatch.CompletedDate = model.CompletedDate; dispatch.Description = model.Description; diff --git a/Api.SeaHavenIndustries/DTOs/Dispatch_DTO.cs b/Api.SeaHavenIndustries/DTOs/Dispatch_DTO.cs index 4601436..49f17f0 100644 --- a/Api.SeaHavenIndustries/DTOs/Dispatch_DTO.cs +++ b/Api.SeaHavenIndustries/DTOs/Dispatch_DTO.cs @@ -6,6 +6,8 @@ namespace Api.SeaHavenIndustries.DTOs public List VendorIds { get; set; } = new(); public decimal? NTEAmount { get; set; } public DateTime? ScheduledDate { get; set; } + public DateTime? EstimatedArrivalAt { get; set; } + public bool EtaManualOverride { get; set; } public string? Description { get; set; } public int? TaskListTemplateId { get; set; } public List? CustomChecklistItems { get; set; } diff --git a/Api.SeaHavenIndustries/DTOs/UpdateDispatch_DTO.cs b/Api.SeaHavenIndustries/DTOs/UpdateDispatch_DTO.cs index 31770e1..40052f1 100644 --- a/Api.SeaHavenIndustries/DTOs/UpdateDispatch_DTO.cs +++ b/Api.SeaHavenIndustries/DTOs/UpdateDispatch_DTO.cs @@ -6,6 +6,8 @@ namespace Api.SeaHavenIndustries.DTOs public string? Status { get; set; } public decimal? NTEAmount { get; set; } public DateTime? ScheduledDate { get; set; } + public DateTime? EstimatedArrivalAt { get; set; } + public bool EtaManualOverride { get; set; } public DateTime? CompletedDate { get; set; } public string? Description { get; set; } } diff --git a/Api.SeaHavenIndustries/Helper/ClamAvDocumentScanner.cs b/Api.SeaHavenIndustries/Helper/ClamAvDocumentScanner.cs new file mode 100644 index 0000000..7280c9f --- /dev/null +++ b/Api.SeaHavenIndustries/Helper/ClamAvDocumentScanner.cs @@ -0,0 +1,70 @@ +using System.Buffers.Binary; +using System.Net.Sockets; +using System.Text; + +namespace Api.SeaHavenIndustries.Helper +{ + public enum DocumentScanResult + { + Passed, + Infected, + Unavailable + } + + public interface IDocumentScanner + { + Task ScanAsync(string path, CancellationToken cancellationToken); + } + + public class ClamAvDocumentScanner : IDocumentScanner + { + private readonly IConfiguration _configuration; + + public ClamAvDocumentScanner(IConfiguration configuration) + { + _configuration = configuration; + } + + public async Task ScanAsync(string path, CancellationToken cancellationToken) + { + var host = _configuration["ClamAV:Host"]; + var port = _configuration.GetValue("ClamAV:Port") ?? 3310; + if (string.IsNullOrWhiteSpace(host) || !File.Exists(path)) + return DocumentScanResult.Unavailable; + + try + { + using var client = new TcpClient(); + await client.ConnectAsync(host, port, cancellationToken); + await using var network = client.GetStream(); + await network.WriteAsync(Encoding.ASCII.GetBytes("zINSTREAM\0"), cancellationToken); + await using var file = File.OpenRead(path); + var buffer = new byte[64 * 1024]; + while (true) + { + var count = await file.ReadAsync(buffer, cancellationToken); + if (count == 0) break; + var length = new byte[4]; + BinaryPrimitives.WriteInt32BigEndian(length, count); + await network.WriteAsync(length, cancellationToken); + await network.WriteAsync(buffer.AsMemory(0, count), cancellationToken); + } + await network.WriteAsync(new byte[4], cancellationToken); + await network.FlushAsync(cancellationToken); + + var responseBuffer = new byte[1024]; + var responseLength = await network.ReadAsync(responseBuffer, cancellationToken); + var response = Encoding.ASCII.GetString(responseBuffer, 0, responseLength); + if (response.Contains("FOUND", StringComparison.OrdinalIgnoreCase)) + return DocumentScanResult.Infected; + return response.Contains("OK", StringComparison.OrdinalIgnoreCase) + ? DocumentScanResult.Passed + : DocumentScanResult.Unavailable; + } + catch (Exception) when (!cancellationToken.IsCancellationRequested) + { + return DocumentScanResult.Unavailable; + } + } + } +} diff --git a/Api.SeaHavenIndustries/Helper/VendorDocumentScanWorker.cs b/Api.SeaHavenIndustries/Helper/VendorDocumentScanWorker.cs new file mode 100644 index 0000000..0f4adcd --- /dev/null +++ b/Api.SeaHavenIndustries/Helper/VendorDocumentScanWorker.cs @@ -0,0 +1,75 @@ +using Data.SeaHavenIndustries; +using Microsoft.EntityFrameworkCore; + +namespace Api.SeaHavenIndustries.Helper +{ + public class VendorDocumentScanWorker : BackgroundService + { + private readonly IServiceScopeFactory _scopeFactory; + private readonly IWebHostEnvironment _environment; + private readonly ILogger _logger; + + public VendorDocumentScanWorker( + IServiceScopeFactory scopeFactory, + IWebHostEnvironment environment, + ILogger logger) + { + _scopeFactory = scopeFactory; + _environment = environment; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await ScanPendingAsync(stoppingToken); + await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); + } + } + + private async Task ScanPendingAsync(CancellationToken cancellationToken) + { + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var scanner = scope.ServiceProvider.GetRequiredService(); + var pending = await db.VendorCompletionDocuments + .Where(document => document.ScanStatus == "Pending") + .OrderBy(document => document.CreatedDate) + .Take(20) + .ToListAsync(cancellationToken); + + foreach (var document in pending) + { + var path = VendorDocumentStorage.ResolvePath(_environment.ContentRootPath, document); + var result = await scanner.ScanAsync(path, cancellationToken); + if (result == DocumentScanResult.Unavailable) + { + _logger.LogWarning("Vendor document scan service unavailable; document {DocumentId} remains quarantined", document.Id); + continue; + } + + document.ScanStatus = result == DocumentScanResult.Passed ? "Passed" : "Rejected"; + document.ReviewStatus = result == DocumentScanResult.Passed ? "Processing" : "Rejected"; + document.RejectionReason = result == DocumentScanResult.Infected + ? "The upload failed malware scanning." + : document.RejectionReason; + document.ScannedAt = DateTime.UtcNow; + document.LastModificationTime = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + + if (result == DocumentScanResult.Infected && File.Exists(path)) + File.Delete(path); + } + } + } + + public static class VendorDocumentStorage + { + public static string Root(string contentRoot) => + Path.Combine(contentRoot, "App_Data", "vendor-documents"); + + public static string ResolvePath(string contentRoot, VendorCompletionDocument document) => + Path.Combine(Root(contentRoot), document.VendorId.ToString(), document.DispatchId.ToString(), document.StoredFileName); + } +} diff --git a/Api.SeaHavenIndustries/Program.cs b/Api.SeaHavenIndustries/Program.cs index 6dc0e95..17022c8 100644 --- a/Api.SeaHavenIndustries/Program.cs +++ b/Api.SeaHavenIndustries/Program.cs @@ -39,6 +39,9 @@ option.AddDefaultPolicy(builder => builder.AllowAnyOrigin().AllowAnyHeader().All builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddSingleton(TimeProvider.System); +builder.Services.AddScoped(); +builder.Services.AddHostedService(); builder.Services.AddSingleton(sp => { var zipService = new Api.SeaHavenIndustries.Helper.ZipCodeDistance(); diff --git a/Data.SeaHavenIndustries/Auth/ApplicationDbContext.cs b/Data.SeaHavenIndustries/Auth/ApplicationDbContext.cs index c7c85c6..9511955 100644 --- a/Data.SeaHavenIndustries/Auth/ApplicationDbContext.cs +++ b/Data.SeaHavenIndustries/Auth/ApplicationDbContext.cs @@ -61,6 +61,16 @@ namespace Data.SeaHavenIndustries .WithMany(c => c.Vendors) .HasForeignKey(v => v.CompanyId) .OnDelete(DeleteBehavior.Restrict); + + builder.Entity() + .HasIndex(p => new { p.LocationId, p.VendorId, p.Trade }) + .IsUnique() + .HasFilter("[Trade] IS NOT NULL"); + + builder.Entity() + .HasIndex(p => new { p.LocationId, p.VendorId }) + .IsUnique() + .HasFilter("[Trade] IS NULL"); } public DbSet Categories { get; set; } public DbSet Locations { get; set; } @@ -81,6 +91,8 @@ namespace Data.SeaHavenIndustries public DbSet VendorCompanies { get; set; } public DbSet VendorAuditLogs { get; set; } public DbSet VendorAccessTokens { get; set; } + public DbSet VendorCompletionDocuments { get; set; } + public DbSet SitePreferredVendors { get; set; } public DbSet Dispatches { get; set; } public DbSet DispatchWorkOrders { get; set; } public DbSet DispatchChecklistItems { get; set; } diff --git a/Data.SeaHavenIndustries/Migrations/20260723220614_AddVendorRoadmapOperations.Designer.cs b/Data.SeaHavenIndustries/Migrations/20260723220614_AddVendorRoadmapOperations.Designer.cs new file mode 100644 index 0000000..9dc7d67 --- /dev/null +++ b/Data.SeaHavenIndustries/Migrations/20260723220614_AddVendorRoadmapOperations.Designer.cs @@ -0,0 +1,3441 @@ +// +using System; +using Data.SeaHavenIndustries; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.SeaHavenIndustries.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260723220614_AddVendorRoadmapOperations")] + partial class AddVendorRoadmapOperations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Data.SeaHavenIndustries.Accounts", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("About") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("CurrencyCode") + .HasColumnType("nvarchar(max)"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("FacebookUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Importance") + .HasColumnType("int"); + + b.Property("IndustryId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LinkedInUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerEmployeeId") + .HasColumnType("int"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("PrivacySetting") + .HasColumnType("nvarchar(max)"); + + b.Property("ServiceInstructions") + .HasColumnType("nvarchar(max)"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("TerritoryId") + .HasColumnType("int"); + + b.Property("TimeZone") + .HasColumnType("nvarchar(max)"); + + b.Property("TwitterUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Website") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Addresses", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("Address1") + .HasColumnType("nvarchar(max)"); + + b.Property("Address2") + .HasColumnType("nvarchar(max)"); + + b.Property("AddressType") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactId") + .HasColumnType("int"); + + b.Property("Country") + .HasColumnType("nvarchar(max)"); + + b.Property("County") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("Zip") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ContactId"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Color") + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Contact") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeletedDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("Initials") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("Locations") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UniqueName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserImage") + .HasColumnType("nvarchar(max)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Assets", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("AssetCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("LocationId"); + + b.ToTable("Assets"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Comments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CommentType") + .HasColumnType("nvarchar(max)"); + + b.Property("Commenter") + .HasColumnType("nvarchar(max)"); + + b.Property("Commenttext") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("Documents") + .HasColumnType("nvarchar(max)"); + + b.Property("ExternalCommentId") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("RecordType") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("WorkerOrderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DispatchId"); + + b.HasIndex("UserId"); + + b.HasIndex("WorkerOrderId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.ContactDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("About") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DateOfBirth") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("FacebookUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Gender") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LinkedInUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("PrivacySetting") + .HasColumnType("nvarchar(max)"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("Territory") + .HasColumnType("nvarchar(max)"); + + b.Property("TwitterUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactDetails"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Contacts", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("Address1") + .HasColumnType("nvarchar(max)"); + + b.Property("Address2") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactType") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("FacebookUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("LinkedInUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("MiddleName") + .HasColumnType("nvarchar(max)"); + + b.Property("Owner") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("TwitterUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Zip") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("LocationId"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Dispatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AcceptToken") + .HasColumnType("nvarchar(max)"); + + b.Property("AcknowledgedAt") + .HasColumnType("datetime2"); + + b.Property("CommercialStatusUpdatedAt") + .HasColumnType("datetime2"); + + b.Property("CompletedDate") + .HasColumnType("datetime2"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DispatchNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("DispatchedAt") + .HasColumnType("datetime2"); + + b.Property("EmailSent") + .HasColumnType("bit"); + + b.Property("EstimatedArrivalAt") + .HasColumnType("datetime2"); + + b.Property("EtaManualOverride") + .HasColumnType("bit"); + + b.Property("InvoiceNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("NTEAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("PONumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReplyToAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("VerifiedAt") + .HasColumnType("datetime2"); + + b.Property("VerifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("VendorId"); + + b.HasIndex("WorkOrderId"); + + b.ToTable("Dispatches"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchChecklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("CompletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("IsCompleted") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ItemText") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DispatchId"); + + b.HasIndex("WorkOrderId"); + + b.ToTable("DispatchChecklistItems"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchSignoff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Signature") + .HasColumnType("nvarchar(max)"); + + b.Property("SignatureMethod") + .HasColumnType("nvarchar(max)"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignoffType") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DispatchId"); + + b.ToTable("DispatchSignoffs"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchUpliftRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("CurrentNTE") + .HasColumnType("decimal(18,2)"); + + b.Property("DecidedAt") + .HasColumnType("datetime2"); + + b.Property("DecidedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DecisionNote") + .HasColumnType("nvarchar(max)"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("RequestedByVendorName") + .HasColumnType("nvarchar(max)"); + + b.Property("RequestedNTE") + .HasColumnType("decimal(18,2)"); + + b.Property("RequiredTier") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("VendorReason") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DispatchId", "Status") + .IsUnique() + .HasFilter("[Status] = 'Pending'"); + + b.ToTable("DispatchUpliftRequests"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DispatchId"); + + b.HasIndex("WorkOrderId"); + + b.ToTable("DispatchWorkOrders"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DropdownOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("ParentValue") + .HasColumnType("nvarchar(max)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DropdownOptions"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("About") + .HasColumnType("nvarchar(max)"); + + b.Property("CanChangeProviderNumber") + .HasColumnType("bit"); + + b.Property("ConfirmUserName") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DateOfBirth") + .HasColumnType("datetime2"); + + b.Property("DepartmentId") + .HasColumnType("int"); + + b.Property("EmployeeNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("FacebookUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("HireDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobTitleId") + .HasColumnType("int"); + + b.Property("LastLogin") + .HasColumnType("datetime2"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LinkedInUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("MiddleName") + .HasColumnType("nvarchar(max)"); + + b.Property("PermissionGroupId") + .HasColumnType("int"); + + b.Property("RegionId") + .HasColumnType("int"); + + b.Property("SMSAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TimeZone") + .HasColumnType("nvarchar(max)"); + + b.Property("TwitterUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("UserName") + .HasColumnType("nvarchar(max)"); + + b.Property("Website") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("JobTitleId"); + + b.HasIndex("RegionId"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.EmployeeAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CountryCode") + .HasColumnType("nvarchar(max)"); + + b.Property("CountyId") + .HasColumnType("int"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("StateCode") + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.Property("StreetLineTwo") + .HasColumnType("nvarchar(max)"); + + b.Property("Zip") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.ToTable("EmployeeAddresses"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.EmployeeEmail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailTypeId") + .HasColumnType("int"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("IsPrimary") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EmployeeEmails"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.EmployeePhone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("Extension") + .HasColumnType("nvarchar(max)"); + + b.Property("IsPrimary") + .HasColumnType("bit"); + + b.Property("Number") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneTypeId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EmployeePhones"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Events", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AllDay") + .HasColumnType("bit"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EndTime") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("EventColor") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Location") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("RecordID") + .HasColumnType("int"); + + b.Property("RecordName") + .HasColumnType("nvarchar(max)"); + + b.Property("RecordTargetType") + .HasColumnType("nvarchar(max)"); + + b.Property("ReminderMinutes") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("StartTime") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.FollowUps", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ScheduleDate") + .HasColumnType("datetime2"); + + b.Property("ScheduleStartTime") + .HasColumnType("time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("LocationId"); + + b.HasIndex("WorkOrderId"); + + b.ToTable("FollowUps"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.ForgetPasswordCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ForgetPasswordCodes"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.JobTitle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("JobTitles"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Locations", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address1") + .HasColumnType("nvarchar(max)"); + + b.Property("Address2") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Latitude") + .HasColumnType("nvarchar(max)"); + + b.Property("Longitude") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("Zip") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.PMSchedules", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AssetId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Frequency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AssetId"); + + b.HasIndex("LocationId"); + + b.ToTable("PMSchedules"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Quotes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BillingAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("PO") + .HasColumnType("nvarchar(max)"); + + b.Property("ProjectName") + .HasColumnType("nvarchar(max)"); + + b.Property("QuotId") + .HasColumnType("nvarchar(max)"); + + b.Property("ShippingAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("SiteId") + .HasColumnType("nvarchar(max)"); + + b.Property("SiteName") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TT") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkorderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("WorkorderId"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.QuotesLineItems", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Descriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Lineitem") + .HasColumnType("decimal(18,2)"); + + b.Property("LineitemNumber") + .HasColumnType("int"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("QuoteId"); + + b.ToTable("QuotesLines"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Regions"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.SitePreferredVendor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Trade") + .HasColumnType("nvarchar(450)"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("VendorId"); + + b.HasIndex("LocationId", "VendorId") + .IsUnique() + .HasFilter("[Trade] IS NULL"); + + b.HasIndex("LocationId", "VendorId", "Trade") + .IsUnique() + .HasFilter("[Trade] IS NOT NULL"); + + b.ToTable("SitePreferredVendors"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.TaskListTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TaskListTemplates"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.TaskListTemplateItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ItemText") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TaskListTemplateId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("TaskListTemplateId"); + + b.ToTable("TaskListTemplateItems"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Template", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AfterPhotoAttachment") + .HasColumnType("nvarchar(max)"); + + b.Property("AfterPhotoIssue") + .HasColumnType("nvarchar(max)"); + + b.Property("AssignTo") + .HasColumnType("nvarchar(450)"); + + b.Property("Attachments") + .HasColumnType("nvarchar(max)"); + + b.Property("BeforPhotoAttachment") + .HasColumnType("nvarchar(max)"); + + b.Property("BeforPhotoIssue") + .HasColumnType("nvarchar(max)"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ContactId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("PO") + .HasColumnType("nvarchar(max)"); + + b.Property("Priority") + .HasColumnType("nvarchar(max)"); + + b.Property("SignOffAttachment") + .HasColumnType("nvarchar(max)"); + + b.Property("SignOffName") + .HasColumnType("nvarchar(max)"); + + b.Property("SignOffSignature") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TT") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkerOrderNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkerOrderTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AssignTo"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ContactId"); + + b.HasIndex("LocationId"); + + b.ToTable("Templates"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Vendor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("AvailabilityStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvailabilityUpdatedAt") + .HasColumnType("datetime2"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactName") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("GoogleMapsUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("PreferredContact") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("TradeSpecialties") + .HasColumnType("nvarchar(max)"); + + b.Property("Zip") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("Vendors"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.VendorAccessToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IssuedAt") + .HasColumnType("datetime2"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LastUsedAt") + .HasColumnType("datetime2"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("VendorId"); + + b.ToTable("VendorAccessTokens"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.VendorAuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Actor") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("FieldName") + .HasColumnType("nvarchar(max)"); + + b.Property("NewValue") + .HasColumnType("nvarchar(max)"); + + b.Property("OldValue") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("VendorId"); + + b.ToTable("VendorAuditLogs"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.VendorCompany", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("GoogleMapsUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NormalizedName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("Zip") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique(); + + b.ToTable("VendorCompanies"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.VendorCompletionDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("OriginalFileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RejectionReason") + .HasColumnType("nvarchar(max)"); + + b.Property("ReplacesDocumentId") + .HasColumnType("int"); + + b.Property("ReviewStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime2"); + + b.Property("ScanStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ScannedAt") + .HasColumnType("datetime2"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("StoredFileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DispatchId"); + + b.HasIndex("VendorId"); + + b.HasIndex("WorkOrderId"); + + b.ToTable("VendorCompletionDocuments"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AfterPhotoAttachment") + .HasColumnType("nvarchar(max)"); + + b.Property("AfterPhotoIssue") + .HasColumnType("nvarchar(max)"); + + b.Property("AssignDate") + .HasColumnType("date"); + + b.Property("AssignTo") + .HasColumnType("nvarchar(450)"); + + b.Property("Attachments") + .HasColumnType("nvarchar(max)"); + + b.Property("AvettaTask") + .HasColumnType("nvarchar(max)"); + + b.Property("BeforPhotoAttachment") + .HasColumnType("nvarchar(max)"); + + b.Property("BeforPhotoIssue") + .HasColumnType("nvarchar(max)"); + + b.Property("Building") + .HasColumnType("nvarchar(max)"); + + b.Property("CarriedOver") + .HasColumnType("int"); + + b.Property("CompletedDate") + .HasColumnType("datetime2"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Customer") + .HasColumnType("nvarchar(max)"); + + b.Property("DateReported") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DocStatus") + .HasColumnType("int"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("ExternalWorkOrderId") + .HasColumnType("nvarchar(max)"); + + b.Property("InternalWONumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LegacyStatus") + .HasColumnType("nvarchar(max)"); + + b.Property("LifecycleStatus") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("OperationalFlags") + .HasColumnType("int"); + + b.Property("OriginalDate") + .HasColumnType("date"); + + b.Property("OriginalWeek") + .HasColumnType("date"); + + b.Property("PO") + .HasColumnType("nvarchar(max)"); + + b.Property("PrimaryDispatchId") + .HasColumnType("int"); + + b.Property("Priority") + .HasColumnType("nvarchar(max)"); + + b.Property("Problem") + .HasColumnType("nvarchar(max)"); + + b.Property("RescheduleCount") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion"); + + b.Property("ScheduleWeekOnly") + .HasColumnType("bit"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("ScheduledEnd") + .HasColumnType("datetime2"); + + b.Property("ScheduledStart") + .HasColumnType("datetime2"); + + b.Property("Service") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Severity") + .HasColumnType("nvarchar(max)"); + + b.Property("SignOffAttachment") + .HasColumnType("nvarchar(max)"); + + b.Property("SignOffName") + .HasColumnType("nvarchar(max)"); + + b.Property("SignOffSignature") + .HasColumnType("nvarchar(max)"); + + b.Property("SiteCode") + .HasColumnType("nvarchar(max)"); + + b.Property("Source") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceEmailS3Key") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("SubTrade") + .HasColumnType("nvarchar(max)"); + + b.Property("TT") + .HasColumnType("nvarchar(max)"); + + b.Property("TargetWeek") + .HasColumnType("date"); + + b.Property("Trade") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorNTE") + .HasColumnType("decimal(18,2)"); + + b.Property("WorkOrderType") + .HasColumnType("int"); + + b.Property("WorkerOrderNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkerOrderTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.Property("istemplate") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AssignTo"); + + b.HasIndex("LocationId"); + + b.HasIndex("PrimaryDispatchId"); + + b.ToTable("workOrders"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderAttachments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attachments") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("WorkorderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("WorkorderId"); + + b.ToTable("workOrderAttachments"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderAuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("nvarchar(max)"); + + b.Property("ActorType") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("EventType") + .HasColumnType("nvarchar(max)"); + + b.Property("FieldName") + .HasColumnType("nvarchar(max)"); + + b.Property("NewValue") + .HasColumnType("nvarchar(max)"); + + b.Property("OldValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("WorkOrderId"); + + b.ToTable("WorkOrderAuditLogs"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderCategories", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("WorkorderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("WorkorderId"); + + b.ToTable("workOrderCategories"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderContacts", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContactId") + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkorderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("WorkorderId"); + + b.ToTable("WorkOrderContacts"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderFieldLock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FieldName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("LockedAt") + .HasColumnType("datetime2"); + + b.Property("LockedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkOrderId", "FieldName") + .IsUnique(); + + b.ToTable("WorkOrderFieldLocks"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Addresses", b => + { + b.HasOne("Data.SeaHavenIndustries.Accounts", "Account") + .WithMany("Addresses") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Contacts", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Account"); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Assets", b => + { + b.HasOne("Data.SeaHavenIndustries.Accounts", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Locations", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Account"); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Comments", b => + { + b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch") + .WithMany("Comments") + .HasForeignKey("DispatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", "ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("Comments") + .HasForeignKey("WorkerOrderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ApplicationUser"); + + b.Navigation("Dispatch"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.ContactDetails", b => + { + b.HasOne("Data.SeaHavenIndustries.Contacts", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Contacts", b => + { + b.HasOne("Data.SeaHavenIndustries.Accounts", "Account") + .WithMany("Contacts") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Locations", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Account"); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Dispatch", b => + { + b.HasOne("Data.SeaHavenIndustries.Vendor", "Vendor") + .WithMany("Dispatches") + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("Dispatches") + .HasForeignKey("WorkOrderId"); + + b.Navigation("Vendor"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchChecklistItem", b => + { + b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch") + .WithMany("ChecklistItems") + .HasForeignKey("DispatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany() + .HasForeignKey("WorkOrderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Dispatch"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchSignoff", b => + { + b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch") + .WithMany("Signoffs") + .HasForeignKey("DispatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dispatch"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchUpliftRequest", b => + { + b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch") + .WithMany() + .HasForeignKey("DispatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dispatch"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b => + { + b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch") + .WithMany("DispatchWorkOrders") + .HasForeignKey("DispatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany() + .HasForeignKey("WorkOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dispatch"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Employee", b => + { + b.HasOne("Data.SeaHavenIndustries.Department", "Department") + .WithMany("Employees") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.JobTitle", "JobTitle") + .WithMany("Employees") + .HasForeignKey("JobTitleId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Region", "Region") + .WithMany("Employees") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Department"); + + b.Navigation("JobTitle"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.EmployeeAddress", b => + { + b.HasOne("Data.SeaHavenIndustries.Employee", "Employee") + .WithOne("Address") + .HasForeignKey("Data.SeaHavenIndustries.EmployeeAddress", "EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.EmployeeEmail", b => + { + b.HasOne("Data.SeaHavenIndustries.Employee", "Employee") + .WithMany("Emails") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.EmployeePhone", b => + { + b.HasOne("Data.SeaHavenIndustries.Employee", "Employee") + .WithMany("Phones") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.FollowUps", b => + { + b.HasOne("Data.SeaHavenIndustries.Accounts", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Locations", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany() + .HasForeignKey("WorkOrderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Account"); + + b.Navigation("Employee"); + + b.Navigation("Location"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.PMSchedules", b => + { + b.HasOne("Data.SeaHavenIndustries.Assets", "Asset") + .WithMany("PMSchedules") + .HasForeignKey("AssetId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Locations", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Asset"); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Quotes", b => + { + b.HasOne("Data.SeaHavenIndustries.Contacts", "Contacts") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("Quotes") + .HasForeignKey("WorkorderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Contacts"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.QuotesLineItems", b => + { + b.HasOne("Data.SeaHavenIndustries.Quotes", "Quotes") + .WithMany("LineItems") + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Quotes"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.SitePreferredVendor", b => + { + b.HasOne("Data.SeaHavenIndustries.Locations", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.Vendor", "Vendor") + .WithMany("SitePreferences") + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.TaskListTemplateItem", b => + { + b.HasOne("Data.SeaHavenIndustries.TaskListTemplate", "Template") + .WithMany("Items") + .HasForeignKey("TaskListTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Template", b => + { + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", "AssignToUser") + .WithMany("Templates") + .HasForeignKey("AssignTo") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Category", "Category") + .WithMany("Templates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Contacts", "POC") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Locations", "Locations") + .WithMany("Templates") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignToUser"); + + b.Navigation("Category"); + + b.Navigation("Locations"); + + b.Navigation("POC"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Vendor", b => + { + b.HasOne("Data.SeaHavenIndustries.VendorCompany", "Company") + .WithMany("Vendors") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.VendorAccessToken", b => + { + b.HasOne("Data.SeaHavenIndustries.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.VendorAuditLog", b => + { + b.HasOne("Data.SeaHavenIndustries.Vendor", "Vendor") + .WithMany("AuditLogs") + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.VendorCompletionDocument", b => + { + b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch") + .WithMany("CompletionDocuments") + .HasForeignKey("DispatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany() + .HasForeignKey("WorkOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dispatch"); + + b.Navigation("Vendor"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrder", b => + { + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", "AssignToUser") + .WithMany("WorkOrders") + .HasForeignKey("AssignTo") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Locations", "Locations") + .WithMany("workOrders") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.Dispatch", "PrimaryDispatch") + .WithMany() + .HasForeignKey("PrimaryDispatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignToUser"); + + b.Navigation("Locations"); + + b.Navigation("PrimaryDispatch"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderAttachments", b => + { + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("workOrderAttachments") + .HasForeignKey("WorkorderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderAuditLog", b => + { + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany() + .HasForeignKey("WorkOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderCategories", b => + { + b.HasOne("Data.SeaHavenIndustries.Category", "Category") + .WithMany("WorkOrderCategories") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("WorkOrderCategories") + .HasForeignKey("WorkorderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Category"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderContacts", b => + { + b.HasOne("Data.SeaHavenIndustries.Contacts", "POC") + .WithMany("WorkOrderContacts") + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("WorkOrderContacts") + .HasForeignKey("WorkorderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("POC"); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderFieldLock", b => + { + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany("FieldLocks") + .HasForeignKey("WorkOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("WorkOrder"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Data.SeaHavenIndustries.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Accounts", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.ApplicationUser", b => + { + b.Navigation("Templates"); + + b.Navigation("WorkOrders"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Assets", b => + { + b.Navigation("PMSchedules"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Category", b => + { + b.Navigation("Templates"); + + b.Navigation("WorkOrderCategories"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Contacts", b => + { + b.Navigation("WorkOrderContacts"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Department", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Dispatch", b => + { + b.Navigation("ChecklistItems"); + + b.Navigation("Comments"); + + b.Navigation("CompletionDocuments"); + + b.Navigation("DispatchWorkOrders"); + + b.Navigation("Signoffs"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Employee", b => + { + b.Navigation("Address"); + + b.Navigation("Emails"); + + b.Navigation("Phones"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.JobTitle", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Locations", b => + { + b.Navigation("Templates"); + + b.Navigation("workOrders"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Quotes", b => + { + b.Navigation("LineItems"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Region", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.TaskListTemplate", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.Vendor", b => + { + b.Navigation("AuditLogs"); + + b.Navigation("Dispatches"); + + b.Navigation("SitePreferences"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.VendorCompany", b => + { + b.Navigation("Vendors"); + }); + + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrder", b => + { + b.Navigation("Comments"); + + b.Navigation("Dispatches"); + + b.Navigation("FieldLocks"); + + b.Navigation("Quotes"); + + b.Navigation("WorkOrderCategories"); + + b.Navigation("WorkOrderContacts"); + + b.Navigation("workOrderAttachments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data.SeaHavenIndustries/Migrations/20260723220614_AddVendorRoadmapOperations.cs b/Data.SeaHavenIndustries/Migrations/20260723220614_AddVendorRoadmapOperations.cs new file mode 100644 index 0000000..bea27f8 --- /dev/null +++ b/Data.SeaHavenIndustries/Migrations/20260723220614_AddVendorRoadmapOperations.cs @@ -0,0 +1,230 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.SeaHavenIndustries.Migrations +{ + /// + public partial class AddVendorRoadmapOperations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AvailabilityStatus", + table: "Vendors", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "AvailabilityUpdatedAt", + table: "Vendors", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "CommercialStatusUpdatedAt", + table: "Dispatches", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "EstimatedArrivalAt", + table: "Dispatches", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "EtaManualOverride", + table: "Dispatches", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "InvoiceNumber", + table: "Dispatches", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "InvoiceStatus", + table: "Dispatches", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "PaymentStatus", + table: "Dispatches", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateTable( + name: "SitePreferredVendors", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + LocationId = table.Column(type: "int", nullable: false), + VendorId = table.Column(type: "int", nullable: false), + Trade = table.Column(type: "nvarchar(450)", nullable: true), + SortOrder = table.Column(type: "int", nullable: false), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: true), + createdby = table.Column(type: "nvarchar(max)", nullable: true), + DeleterUserId = table.Column(type: "nvarchar(max)", nullable: true), + DeletionTime = table.Column(type: "datetime2", nullable: true), + CreatedDate = table.Column(type: "datetime2", nullable: true), + LastModificationTime = table.Column(type: "datetime2", nullable: true), + LastModifierUserId = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SitePreferredVendors", x => x.Id); + table.ForeignKey( + name: "FK_SitePreferredVendors_Locations_LocationId", + column: x => x.LocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SitePreferredVendors_Vendors_VendorId", + column: x => x.VendorId, + principalTable: "Vendors", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "VendorCompletionDocuments", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + VendorId = table.Column(type: "int", nullable: false), + DispatchId = table.Column(type: "int", nullable: false), + WorkOrderId = table.Column(type: "int", nullable: false), + OriginalFileName = table.Column(type: "nvarchar(max)", nullable: false), + StoredFileName = table.Column(type: "nvarchar(max)", nullable: false), + ContentType = table.Column(type: "nvarchar(max)", nullable: false), + SizeBytes = table.Column(type: "bigint", nullable: false), + ScanStatus = table.Column(type: "nvarchar(max)", nullable: false), + ReviewStatus = table.Column(type: "nvarchar(max)", nullable: false), + RejectionReason = table.Column(type: "nvarchar(max)", nullable: true), + Version = table.Column(type: "int", nullable: false), + ReplacesDocumentId = table.Column(type: "int", nullable: true), + ScannedAt = table.Column(type: "datetime2", nullable: true), + ReviewedAt = table.Column(type: "datetime2", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: true), + createdby = table.Column(type: "nvarchar(max)", nullable: true), + DeleterUserId = table.Column(type: "nvarchar(max)", nullable: true), + DeletionTime = table.Column(type: "datetime2", nullable: true), + CreatedDate = table.Column(type: "datetime2", nullable: true), + LastModificationTime = table.Column(type: "datetime2", nullable: true), + LastModifierUserId = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_VendorCompletionDocuments", x => x.Id); + table.ForeignKey( + name: "FK_VendorCompletionDocuments_Dispatches_DispatchId", + column: x => x.DispatchId, + principalTable: "Dispatches", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_VendorCompletionDocuments_Vendors_VendorId", + column: x => x.VendorId, + principalTable: "Vendors", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_VendorCompletionDocuments_workOrders_WorkOrderId", + column: x => x.WorkOrderId, + principalTable: "workOrders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_SitePreferredVendors_LocationId_VendorId", + table: "SitePreferredVendors", + columns: new[] { "LocationId", "VendorId" }, + unique: true, + filter: "[Trade] IS NULL"); + + migrationBuilder.CreateIndex( + name: "IX_SitePreferredVendors_LocationId_VendorId_Trade", + table: "SitePreferredVendors", + columns: new[] { "LocationId", "VendorId", "Trade" }, + unique: true, + filter: "[Trade] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_SitePreferredVendors_VendorId", + table: "SitePreferredVendors", + column: "VendorId"); + + migrationBuilder.CreateIndex( + name: "IX_VendorCompletionDocuments_DispatchId", + table: "VendorCompletionDocuments", + column: "DispatchId"); + + migrationBuilder.CreateIndex( + name: "IX_VendorCompletionDocuments_VendorId", + table: "VendorCompletionDocuments", + column: "VendorId"); + + migrationBuilder.CreateIndex( + name: "IX_VendorCompletionDocuments_WorkOrderId", + table: "VendorCompletionDocuments", + column: "WorkOrderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SitePreferredVendors"); + + migrationBuilder.DropTable( + name: "VendorCompletionDocuments"); + + migrationBuilder.DropColumn( + name: "AvailabilityStatus", + table: "Vendors"); + + migrationBuilder.DropColumn( + name: "AvailabilityUpdatedAt", + table: "Vendors"); + + migrationBuilder.DropColumn( + name: "CommercialStatusUpdatedAt", + table: "Dispatches"); + + migrationBuilder.DropColumn( + name: "EstimatedArrivalAt", + table: "Dispatches"); + + migrationBuilder.DropColumn( + name: "EtaManualOverride", + table: "Dispatches"); + + migrationBuilder.DropColumn( + name: "InvoiceNumber", + table: "Dispatches"); + + migrationBuilder.DropColumn( + name: "InvoiceStatus", + table: "Dispatches"); + + migrationBuilder.DropColumn( + name: "PaymentStatus", + table: "Dispatches"); + } + } +} diff --git a/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs b/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs index 09b4ef3..33dd3a9 100644 --- a/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs @@ -631,6 +631,9 @@ namespace Data.SeaHavenIndustries.Migrations b.Property("AcknowledgedAt") .HasColumnType("datetime2"); + b.Property("CommercialStatusUpdatedAt") + .HasColumnType("datetime2"); + b.Property("CompletedDate") .HasColumnType("datetime2"); @@ -655,6 +658,19 @@ namespace Data.SeaHavenIndustries.Migrations b.Property("EmailSent") .HasColumnType("bit"); + b.Property("EstimatedArrivalAt") + .HasColumnType("datetime2"); + + b.Property("EtaManualOverride") + .HasColumnType("bit"); + + b.Property("InvoiceNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .HasColumnType("bit"); @@ -670,6 +686,10 @@ namespace Data.SeaHavenIndustries.Migrations b.Property("PONumber") .HasColumnType("nvarchar(max)"); + b.Property("PaymentStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("ReplyToAddress") .HasColumnType("nvarchar(max)"); @@ -1147,6 +1167,86 @@ namespace Data.SeaHavenIndustries.Migrations b.ToTable("EmployeePhones"); }); + modelBuilder.Entity("Data.SeaHavenIndustries.Events", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AllDay") + .HasColumnType("bit"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EndTime") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("EventColor") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("Location") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("RecordID") + .HasColumnType("int"); + + b.Property("RecordName") + .HasColumnType("nvarchar(max)"); + + b.Property("RecordTargetType") + .HasColumnType("nvarchar(max)"); + + b.Property("ReminderMinutes") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("StartTime") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Events"); + }); + modelBuilder.Entity("Data.SeaHavenIndustries.FollowUps", b => { b.Property("Id") @@ -1526,6 +1626,65 @@ namespace Data.SeaHavenIndustries.Migrations b.ToTable("Regions"); }); + modelBuilder.Entity("Data.SeaHavenIndustries.SitePreferredVendor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Trade") + .HasColumnType("nvarchar(450)"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("VendorId"); + + b.HasIndex("LocationId", "VendorId") + .IsUnique() + .HasFilter("[Trade] IS NULL"); + + b.HasIndex("LocationId", "VendorId", "Trade") + .IsUnique() + .HasFilter("[Trade] IS NOT NULL"); + + b.ToTable("SitePreferredVendors"); + }); + modelBuilder.Entity("Data.SeaHavenIndustries.TaskListTemplate", b => { b.Property("Id") @@ -1727,6 +1886,13 @@ namespace Data.SeaHavenIndustries.Migrations b.Property("Address") .HasColumnType("nvarchar(max)"); + b.Property("AvailabilityStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvailabilityUpdatedAt") + .HasColumnType("datetime2"); + b.Property("City") .HasColumnType("nvarchar(max)"); @@ -1958,6 +2124,93 @@ namespace Data.SeaHavenIndustries.Migrations b.ToTable("VendorCompanies"); }); + modelBuilder.Entity("Data.SeaHavenIndustries.VendorCompletionDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DeleterUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("DispatchId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2"); + + b.Property("LastModifierUserId") + .HasColumnType("int"); + + b.Property("OriginalFileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RejectionReason") + .HasColumnType("nvarchar(max)"); + + b.Property("ReplacesDocumentId") + .HasColumnType("int"); + + b.Property("ReviewStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime2"); + + b.Property("ScanStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ScannedAt") + .HasColumnType("datetime2"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("StoredFileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WorkOrderId") + .HasColumnType("int"); + + b.Property("createdby") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DispatchId"); + + b.HasIndex("VendorId"); + + b.HasIndex("WorkOrderId"); + + b.ToTable("VendorCompletionDocuments"); + }); + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrder", b => { b.Property("Id") @@ -2796,6 +3049,25 @@ namespace Data.SeaHavenIndustries.Migrations b.Navigation("Quotes"); }); + modelBuilder.Entity("Data.SeaHavenIndustries.SitePreferredVendor", b => + { + b.HasOne("Data.SeaHavenIndustries.Locations", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.Vendor", "Vendor") + .WithMany("SitePreferences") + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("Vendor"); + }); + modelBuilder.Entity("Data.SeaHavenIndustries.TaskListTemplateItem", b => { b.HasOne("Data.SeaHavenIndustries.TaskListTemplate", "Template") @@ -2870,6 +3142,33 @@ namespace Data.SeaHavenIndustries.Migrations b.Navigation("Vendor"); }); + modelBuilder.Entity("Data.SeaHavenIndustries.VendorCompletionDocument", b => + { + b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch") + .WithMany("CompletionDocuments") + .HasForeignKey("DispatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder") + .WithMany() + .HasForeignKey("WorkOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dispatch"); + + b.Navigation("Vendor"); + + b.Navigation("WorkOrder"); + }); + modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrder", b => { b.HasOne("Data.SeaHavenIndustries.ApplicationUser", "AssignToUser") @@ -3060,6 +3359,8 @@ namespace Data.SeaHavenIndustries.Migrations b.Navigation("Comments"); + b.Navigation("CompletionDocuments"); + b.Navigation("DispatchWorkOrders"); b.Navigation("Signoffs"); @@ -3106,6 +3407,8 @@ namespace Data.SeaHavenIndustries.Migrations b.Navigation("AuditLogs"); b.Navigation("Dispatches"); + + b.Navigation("SitePreferences"); }); modelBuilder.Entity("Data.SeaHavenIndustries.VendorCompany", b => diff --git a/Data.SeaHavenIndustries/Models/Dispatch.cs b/Data.SeaHavenIndustries/Models/Dispatch.cs index 831cc17..4e7b927 100644 --- a/Data.SeaHavenIndustries/Models/Dispatch.cs +++ b/Data.SeaHavenIndustries/Models/Dispatch.cs @@ -19,6 +19,12 @@ namespace Data.SeaHavenIndustries public string? Description { get; set; } public string? Status { get; set; } public DateTime? ScheduledDate { get; set; } + public DateTime? EstimatedArrivalAt { get; set; } + public bool EtaManualOverride { get; set; } + public string InvoiceStatus { get; set; } = "Not Submitted"; + public string PaymentStatus { get; set; } = "Unavailable"; + public string? InvoiceNumber { get; set; } + public DateTime? CommercialStatusUpdatedAt { get; set; } public DateTime? CompletedDate { get; set; } public DateTime? DispatchedAt { get; set; } public DateTime? AcknowledgedAt { get; set; } @@ -35,5 +41,6 @@ namespace Data.SeaHavenIndustries public ICollection? DispatchWorkOrders { get; set; } public ICollection? ChecklistItems { get; set; } public ICollection? Signoffs { get; set; } + public ICollection? CompletionDocuments { get; set; } } } diff --git a/Data.SeaHavenIndustries/Models/SitePreferredVendor.cs b/Data.SeaHavenIndustries/Models/SitePreferredVendor.cs new file mode 100644 index 0000000..7dc34eb --- /dev/null +++ b/Data.SeaHavenIndustries/Models/SitePreferredVendor.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Data.SeaHavenIndustries +{ + public class SitePreferredVendor : FullAuditEntity + { + public int LocationId { get; set; } + [ForeignKey(nameof(LocationId))] + public Locations? Location { get; set; } + + public int VendorId { get; set; } + [ForeignKey(nameof(VendorId))] + public Vendor? Vendor { get; set; } + + public string? Trade { get; set; } + public int SortOrder { get; set; } + public string? UpdatedBy { get; set; } + } +} diff --git a/Data.SeaHavenIndustries/Models/Vendor.cs b/Data.SeaHavenIndustries/Models/Vendor.cs index 9f8071a..62ba763 100644 --- a/Data.SeaHavenIndustries/Models/Vendor.cs +++ b/Data.SeaHavenIndustries/Models/Vendor.cs @@ -18,6 +18,8 @@ namespace Data.SeaHavenIndustries public string? Notes { get; set; } public bool IsActive { get; set; } = true; public string? PreferredContact { get; set; } + public string AvailabilityStatus { get; set; } = "Unknown"; + public DateTime? AvailabilityUpdatedAt { get; set; } public int? CompanyId { get; set; } [ForeignKey(nameof(CompanyId))] @@ -27,5 +29,6 @@ namespace Data.SeaHavenIndustries public int TotalJobs { get; set; } public ICollection? Dispatches { get; set; } public ICollection? AuditLogs { get; set; } + public ICollection? SitePreferences { get; set; } } } diff --git a/Data.SeaHavenIndustries/Models/VendorCompletionDocument.cs b/Data.SeaHavenIndustries/Models/VendorCompletionDocument.cs new file mode 100644 index 0000000..2058e00 --- /dev/null +++ b/Data.SeaHavenIndustries/Models/VendorCompletionDocument.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Data.SeaHavenIndustries +{ + public class VendorCompletionDocument : FullAuditEntity + { + public int VendorId { get; set; } + [ForeignKey(nameof(VendorId))] + public Vendor? Vendor { get; set; } + + public int DispatchId { get; set; } + [ForeignKey(nameof(DispatchId))] + public Dispatch? Dispatch { get; set; } + + public int WorkOrderId { get; set; } + [ForeignKey(nameof(WorkOrderId))] + public WorkOrder? WorkOrder { get; set; } + + public string OriginalFileName { get; set; } = ""; + public string StoredFileName { get; set; } = ""; + public string ContentType { get; set; } = ""; + public long SizeBytes { get; set; } + public string ScanStatus { get; set; } = "Pending"; + public string ReviewStatus { get; set; } = "Processing"; + public string? RejectionReason { get; set; } + public int Version { get; set; } = 1; + public int? ReplacesDocumentId { get; set; } + public DateTime? ScannedAt { get; set; } + public DateTime? ReviewedAt { get; set; } + } +} diff --git a/Data.SeaHavenIndustries/Models/WorkerOrder.cs b/Data.SeaHavenIndustries/Models/WorkerOrder.cs index 18e51bf..81c6ad8 100644 --- a/Data.SeaHavenIndustries/Models/WorkerOrder.cs +++ b/Data.SeaHavenIndustries/Models/WorkerOrder.cs @@ -27,6 +27,7 @@ namespace Data.SeaHavenIndustries public string? LegacyStatus { get; set; } public WorkOrderType? WorkOrderType { get; set; } /// SHOC board "Service" column (PM type label). Not Trade/Problem. + [MaxLength(128)] public string? Service { get; set; } public int? PrimaryDispatchId { get; set; } [ForeignKey(nameof(PrimaryDispatchId))]