Add vendor dispatch workflow backend

- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
  HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
This commit is contained in:
Adam Moussa 2026-04-17 11:37:35 -04:00
parent 05b36ef7dd
commit d24c4643f5
12 changed files with 3144 additions and 8 deletions

View file

@ -0,0 +1,176 @@
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
[Route("api/vendors")]
public class VendorController : Controller
{
private readonly ApplicationDbContext _db;
public VendorController(ApplicationDbContext db)
{
_db = db;
}
[HttpGet("GetVendorList")]
public IActionResult GetVendorList(string? search = "", int page = 1, int pageSize = 10)
{
var query = _db.Vendors.Where(v => v.IsActive);
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(v =>
(v.CompanyName ?? "").ToLower().Contains(s) ||
(v.ContactName ?? "").ToLower().Contains(s) ||
(v.Email ?? "").ToLower().Contains(s) ||
(v.TradeSpecialties ?? "").ToLower().Contains(s));
}
var totalCount = query.Count();
var data = query.OrderByDescending(v => v.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(v => new
{
v.Id,
v.CompanyName,
v.ContactName,
v.Email,
v.Phone,
v.TradeSpecialties,
v.IsActive
})
.ToList();
return Ok(new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
});
}
[HttpGet("{id}")]
public async Task<IActionResult> GetVendorById(int id)
{
var vendor = await _db.Vendors.Where(v => v.Id == id).Select(v => new
{
v.Id,
v.CompanyName,
v.ContactName,
v.Email,
v.Phone,
v.Address,
v.City,
v.State,
v.Zip,
v.TradeSpecialties,
v.IsActive
}).FirstOrDefaultAsync();
if (vendor == null)
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
return Ok(vendor);
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] Vendor_DTO model)
{
try
{
var vendor = new Vendor
{
CompanyName = model.CompanyName,
ContactName = model.ContactName,
Email = model.Email,
Phone = model.Phone,
Address = model.Address,
City = model.City,
State = model.State,
Zip = model.Zip,
TradeSpecialties = model.TradeSpecialties,
IsActive = model.IsActive
};
_db.Vendors.Add(vendor);
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Vendor Created", Status = "200" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [FromBody] EditVendor_DTO model)
{
try
{
var exist = await _db.Vendors.FindAsync(id);
if (exist == null)
return BadRequest(new Response { Status = "Error", Message = "Vendor not found" });
exist.CompanyName = model.CompanyName;
exist.ContactName = model.ContactName;
exist.Email = model.Email;
exist.Phone = model.Phone;
exist.Address = model.Address;
exist.City = model.City;
exist.State = model.State;
exist.Zip = model.Zip;
exist.TradeSpecialties = model.TradeSpecialties;
exist.IsActive = model.IsActive;
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Vendor Updated", Status = "200" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var exist = await _db.Vendors.FindAsync(id);
if (exist == null)
return BadRequest(new Response { Status = "Error", Message = "Vendor not found" });
_db.Vendors.Remove(exist);
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Vendor Deleted", Status = "200" });
}
[HttpGet("Dropdown")]
public async Task<IActionResult> GetDropdown([FromQuery] string? trade = null)
{
var query = _db.Vendors.Where(v => v.IsActive);
var vendors = await query.OrderBy(v => v.CompanyName)
.Select(v => new { v.Id, v.CompanyName, v.TradeSpecialties })
.ToListAsync();
if (!string.IsNullOrWhiteSpace(trade))
{
var tradeMatched = vendors.Where(v => (v.TradeSpecialties ?? "").Contains(trade)).ToList();
var rest = vendors.Where(v => !(v.TradeSpecialties ?? "").Contains(trade)).ToList();
vendors = tradeMatched.Concat(rest).ToList();
}
return Ok(vendors);
}
}
}

View file

@ -26,12 +26,14 @@ namespace Api.SeaHavenIndustries.Controllers
private readonly ApplicationDbContext _db;
private readonly IWebHostEnvironment _webHostEnvironment;
IHttpContextAccessor _httpContext;
public WorkOrderController(UserManager<ApplicationUser> userManager, ApplicationDbContext db, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContext)
private readonly Helper.SendMessage _sendMessage;
public WorkOrderController(UserManager<ApplicationUser> userManager, ApplicationDbContext db, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContext, Helper.SendMessage sendMessage)
{
_userManager = userManager;
_db = db;
_webHostEnvironment = webHostEnvironment;
_httpContext = httpContext;
_sendMessage = sendMessage;
}
[HttpPost]
[Route("AddWorkorder")]
@ -593,7 +595,7 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpGet("GetWorkorderById")]
public async Task<IActionResult> GetWorkorderById(int id)
{
var exist = await _db.workOrders.Include(w => w.Locations).Include(w => w.Comments).Include(w => w.Quotes).Where(w => w.Id == id).Select(s => new
var exist = await _db.workOrders.Include(w => w.Locations).Include(w => w.Comments).Include(w => w.Quotes).Include(w => w.Dispatches).ThenInclude(d => d.Vendor).Where(w => w.Id == id).Select(s => new
{
s.Id,
s.InternalWONumber,
@ -639,7 +641,18 @@ namespace Api.SeaHavenIndustries.Controllers
a.Action,
a.CreatedAt,
userName = a.User != null ? a.User.FirstName + " " + a.User.LastName : ""
}).ToList()
}).ToList(),
dispatches = s.Dispatches.Select(d => new {
d.Id,
d.PONumber,
d.NTEAmount,
d.Description,
d.Status,
d.DispatchedAt,
d.EmailSent,
vendorName = d.Vendor != null ? d.Vendor.CompanyName : "",
vendorEmail = d.Vendor != null ? d.Vendor.Email : ""
}).ToList()
}).FirstOrDefaultAsync();
if (exist == null)
{
@ -980,6 +993,125 @@ namespace Api.SeaHavenIndustries.Controllers
}).ToListAsync();
return Ok(data);
}
[HttpPost]
[Route("DispatchToVendor")]
public async Task<IActionResult> DispatchToVendor([FromBody] Dispatch_DTO model)
{
try
{
var workOrder = await _db.workOrders.Include(w => w.Locations)
.FirstOrDefaultAsync(w => w.Id == model.WorkOrderId);
if (workOrder == null)
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
var lastPO = await _db.Dispatches
.Where(d => d.PONumber != null)
.OrderByDescending(d => d.PONumber)
.Select(d => d.PONumber)
.FirstOrDefaultAsync();
int nextPO = 1;
if (lastPO != null && lastPO.StartsWith("VPO-") && int.TryParse(lastPO.Substring(4), out var lastNum))
nextPO = lastNum + 1;
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var dispatches = new List<object>();
foreach (var vendorId in model.VendorIds)
{
var vendor = await _db.Vendors.FindAsync(vendorId);
if (vendor == null) continue;
var poNumber = $"VPO-{nextPO:D5}";
nextPO++;
var replyTo = $"wo-{workOrder.InternalWONumber}@int.seahaven.com";
var dispatch = new Dispatch
{
WorkOrderId = model.WorkOrderId,
VendorId = vendorId,
PONumber = poNumber,
NTEAmount = model.NTEAmount,
Description = model.Description,
Status = "Sent",
DispatchedAt = DateTime.UtcNow,
ReplyToAddress = replyTo
};
var subject = $"[WO-{workOrder.InternalWONumber}] {workOrder.WorkerOrderTitle}";
var locationName = workOrder.Locations?.Name ?? "";
var locationAddr = workOrder.Locations?.Address ?? "";
var html = $@"
<h2>Work Order Dispatch</h2>
<table style='border-collapse:collapse;width:100%;font-family:Arial,sans-serif;'>
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>WO Number</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.InternalWONumber}</td></tr>
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Title</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.WorkerOrderTitle}</td></tr>
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Location</td><td style='padding:8px;border:1px solid #ddd;'>{locationName} {locationAddr}</td></tr>
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Priority</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.Priority}</td></tr>
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Due Date</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.DueDate?.ToString("yyyy-MM-dd") ?? "N/A"}</td></tr>
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Trade</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.Trade}</td></tr>
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Sub-Trade</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.SubTrade}</td></tr>
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Problem</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.Problem}</td></tr>
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>NTE</td><td style='padding:8px;border:1px solid #ddd;'>${model.NTEAmount?.ToString("F2") ?? "N/A"}</td></tr>
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Vendor PO</td><td style='padding:8px;border:1px solid #ddd;'>{poNumber}</td></tr>
</table>
<h3>Description</h3>
<p>{model.Description ?? workOrder.Description}</p>
<hr/>
<p style='color:#999;font-size:12px;'>Reply to this email to communicate about this work order.</p>";
var emailSent = await _sendMessage.SendDispatchEmail(vendor.Email, subject, html, replyTo);
dispatch.EmailSent = emailSent;
_db.Dispatches.Add(dispatch);
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = model.WorkOrderId,
UserId = userId,
FieldName = "Dispatch",
OldValue = "",
NewValue = $"{vendor.CompanyName} ({poNumber})",
Action = "dispatch",
CreatedAt = DateTime.UtcNow
});
dispatches.Add(new { dispatch.Id, poNumber, vendorName = vendor.CompanyName, dispatch.Status });
}
await _db.SaveChangesAsync();
return Ok(new { message = "Dispatched", dispatches });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpGet("GetDispatches/{workOrderId}")]
public async Task<IActionResult> GetDispatches(int workOrderId)
{
var data = await _db.Dispatches
.Where(d => d.WorkOrderId == workOrderId)
.Include(d => d.Vendor)
.OrderByDescending(d => d.DispatchedAt)
.Select(d => new
{
d.Id,
d.PONumber,
d.NTEAmount,
d.Description,
d.Status,
d.DispatchedAt,
d.EmailSent,
vendorName = d.Vendor != null ? d.Vendor.CompanyName : "",
vendorEmail = d.Vendor != null ? d.Vendor.Email : ""
})
.ToListAsync();
return Ok(data);
}
private async Task<string> GenerateInternalWONumber()
{
var lastWO = await _db.workOrders

View file

@ -0,0 +1,10 @@
namespace Api.SeaHavenIndustries.DTOs
{
public class Dispatch_DTO
{
public int WorkOrderId { get; set; }
public List<int> VendorIds { get; set; } = new();
public decimal? NTEAmount { get; set; }
public string? Description { get; set; }
}
}

View file

@ -0,0 +1,21 @@
namespace Api.SeaHavenIndustries.DTOs
{
public class Vendor_DTO
{
public string? CompanyName { get; set; }
public string? ContactName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
public string? TradeSpecialties { get; set; }
public bool IsActive { get; set; } = true;
}
public class EditVendor_DTO : Vendor_DTO
{
public int Id { get; set; }
}
}

View file

@ -98,8 +98,27 @@ namespace Api.SeaHavenIndustries.Helper
return false;
}
}
public async Task<bool> SendDispatchEmail(string emailTo, string subject, string htmlBody, string? replyTo)
{
try
{
var apiKey = _configuration.GetValue<string>("SendGrid:ApiKey");
var client = new SendGridClient(apiKey);
var from = new EmailAddress("tech@seahavenind.com", "Sea Haven Industries");
var to = new EmailAddress(emailTo, "");
var msg = MailHelper.CreateSingleEmail(from, to, subject, "", htmlBody);
if (!string.IsNullOrWhiteSpace(replyTo))
msg.SetReplyTo(new EmailAddress(replyTo));
var response = await client.SendEmailAsync(msg);
return true;
}
catch (Exception ex)
{
return false;
}
}
}
}

View file

@ -39,6 +39,8 @@ namespace Data.SeaHavenIndustries
public DbSet<WorkOrderAttachments> workOrderAttachments { get; set; }
public DbSet<WorkOrderAuditLog> WorkOrderAuditLogs { get; set; }
public DbSet<DropdownOption> DropdownOptions { get; set; }
public DbSet<Vendor> Vendors { get; set; }
public DbSet<Dispatch> Dispatches { get; set; }
// New DbSets for added entities
public DbSet<Accounts> Accounts { get; set; }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,104 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddVendorAndDispatch : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Vendors",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
CompanyName = table.Column<string>(type: "nvarchar(max)", nullable: true),
ContactName = table.Column<string>(type: "nvarchar(max)", nullable: true),
Email = table.Column<string>(type: "nvarchar(max)", nullable: true),
Phone = table.Column<string>(type: "nvarchar(max)", nullable: true),
Address = table.Column<string>(type: "nvarchar(max)", nullable: true),
City = table.Column<string>(type: "nvarchar(max)", nullable: true),
State = table.Column<string>(type: "nvarchar(max)", nullable: true),
Zip = table.Column<string>(type: "nvarchar(max)", nullable: true),
TradeSpecialties = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Vendors", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Dispatches",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
WorkOrderId = table.Column<int>(type: "int", nullable: false),
VendorId = table.Column<int>(type: "int", nullable: false),
PONumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
NTEAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: true),
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
Status = table.Column<string>(type: "nvarchar(max)", nullable: true),
DispatchedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
EmailSent = table.Column<bool>(type: "bit", nullable: false),
ReplyToAddress = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Dispatches", x => x.Id);
table.ForeignKey(
name: "FK_Dispatches_Vendors_VendorId",
column: x => x.VendorId,
principalTable: "Vendors",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Dispatches_workOrders_WorkOrderId",
column: x => x.WorkOrderId,
principalTable: "workOrders",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_Dispatches_VendorId",
table: "Dispatches",
column: "VendorId");
migrationBuilder.CreateIndex(
name: "IX_Dispatches_WorkOrderId",
table: "Dispatches",
column: "WorkOrderId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Dispatches");
migrationBuilder.DropTable(
name: "Vendors");
}
}
}

View file

@ -627,6 +627,71 @@ namespace Data.SeaHavenIndustries.Migrations
b.ToTable("Departments");
});
modelBuilder.Entity("Data.SeaHavenIndustries.Dispatch", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime?>("CreatedDate")
.HasColumnType("datetime2");
b.Property<string>("DeleterUserId")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("DispatchedAt")
.HasColumnType("datetime2");
b.Property<bool>("EmailSent")
.HasColumnType("bit");
b.Property<bool?>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2");
b.Property<int?>("LastModifierUserId")
.HasColumnType("int");
b.Property<decimal?>("NTEAmount")
.HasColumnType("decimal(18,2)");
b.Property<string>("PONumber")
.HasColumnType("nvarchar(max)");
b.Property<string>("ReplyToAddress")
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.HasColumnType("nvarchar(max)");
b.Property<int>("VendorId")
.HasColumnType("int");
b.Property<int>("WorkOrderId")
.HasColumnType("int");
b.Property<string>("createdby")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("VendorId");
b.HasIndex("WorkOrderId");
b.ToTable("Dispatches");
});
modelBuilder.Entity("Data.SeaHavenIndustries.DropdownOption", b =>
{
b.Property<int>("Id")
@ -1409,6 +1474,70 @@ namespace Data.SeaHavenIndustries.Migrations
b.ToTable("Templates");
});
modelBuilder.Entity("Data.SeaHavenIndustries.Vendor", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Address")
.HasColumnType("nvarchar(max)");
b.Property<string>("City")
.HasColumnType("nvarchar(max)");
b.Property<string>("CompanyName")
.HasColumnType("nvarchar(max)");
b.Property<string>("ContactName")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("CreatedDate")
.HasColumnType("datetime2");
b.Property<string>("DeleterUserId")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2");
b.Property<string>("Email")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool?>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2");
b.Property<int?>("LastModifierUserId")
.HasColumnType("int");
b.Property<string>("Phone")
.HasColumnType("nvarchar(max)");
b.Property<string>("State")
.HasColumnType("nvarchar(max)");
b.Property<string>("TradeSpecialties")
.HasColumnType("nvarchar(max)");
b.Property<string>("Zip")
.HasColumnType("nvarchar(max)");
b.Property<string>("createdby")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Vendors");
});
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrder", b =>
{
b.Property<int>("Id")
@ -1932,6 +2061,25 @@ namespace Data.SeaHavenIndustries.Migrations
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")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Vendor");
b.Navigation("WorkOrder");
});
modelBuilder.Entity("Data.SeaHavenIndustries.Employee", b =>
{
b.HasOne("Data.SeaHavenIndustries.Department", "Department")
@ -2302,10 +2450,17 @@ namespace Data.SeaHavenIndustries.Migrations
b.Navigation("Employees");
});
modelBuilder.Entity("Data.SeaHavenIndustries.Vendor", b =>
{
b.Navigation("Dispatches");
});
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrder", b =>
{
b.Navigation("Comments");
b.Navigation("Dispatches");
b.Navigation("Quotes");
b.Navigation("WorkOrderCategories");

View file

@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.SeaHavenIndustries
{
public class Dispatch : FullAuditEntity
{
public int WorkOrderId { get; set; }
[ForeignKey(nameof(WorkOrderId))]
public virtual WorkOrder? WorkOrder { get; set; }
public int VendorId { get; set; }
[ForeignKey(nameof(VendorId))]
public virtual Vendor? Vendor { get; set; }
public string? PONumber { get; set; }
public decimal? NTEAmount { get; set; }
public string? Description { get; set; }
public string? Status { get; set; }
public DateTime? DispatchedAt { get; set; }
public bool EmailSent { get; set; }
public string? ReplyToAddress { get; set; }
}
}

View file

@ -0,0 +1,17 @@
namespace Data.SeaHavenIndustries
{
public class Vendor : FullAuditEntity
{
public string? CompanyName { get; set; }
public string? ContactName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
public string? TradeSpecialties { get; set; }
public bool IsActive { get; set; } = true;
public ICollection<Dispatch>? Dispatches { get; set; }
}
}

View file

@ -56,7 +56,6 @@ namespace Data.SeaHavenIndustries
public ICollection<WorkOrderContacts>? WorkOrderContacts { get; set; }
public ICollection<WorkOrderAttachments>? workOrderAttachments { get; set; }
public ICollection<WorkOrderCategories>? WorkOrderCategories { get; set; }
public ICollection<Dispatch>? Dispatches { get; set; }
}
}