Add configurable dropdowns, new WO fields, and seed data

- Create DropdownOption model with Category, Value, ParentValue for Trade/SubTrade/Problem
- Add DropdownOptionsController with CRUD + ByCategory endpoint with parent filtering
- Add Problem, Trade, SubTrade, VendorNTE, ScheduledDate, CompletedDate, Source to WorkOrder
- Update EditWorkorder_DTO and GetWorkorderById with new fields
- Audit log tracks changes to all new fields
- Seed default Trades (10), SubTrades (20), and Problems (11) on startup
This commit is contained in:
Adam Moussa 2026-04-17 10:40:38 -04:00
parent 78c7850fb2
commit 05b36ef7dd
10 changed files with 2642 additions and 1 deletions

View file

@ -0,0 +1,87 @@
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class DropdownOptionsController : Controller
{
private readonly ApplicationDbContext _db;
public DropdownOptionsController(ApplicationDbContext db)
{
_db = db;
}
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] string? category = null)
{
var query = _db.DropdownOptions.Where(d => d.IsActive);
if (!string.IsNullOrWhiteSpace(category))
query = query.Where(d => d.Category == category);
var data = await query.OrderBy(d => d.Category).ThenBy(d => d.SortOrder).ThenBy(d => d.Value).ToListAsync();
return Ok(data);
}
[HttpGet("ByCategory/{category}")]
public async Task<IActionResult> GetByCategory(string category, [FromQuery] string? parent = null)
{
var query = _db.DropdownOptions.Where(d => d.IsActive && d.Category == category);
if (!string.IsNullOrWhiteSpace(parent))
query = query.Where(d => d.ParentValue == parent);
var data = await query.OrderBy(d => d.SortOrder).ThenBy(d => d.Value).ToListAsync();
return Ok(data);
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] DropdownOption model)
{
if (string.IsNullOrWhiteSpace(model.Category) || string.IsNullOrWhiteSpace(model.Value))
return BadRequest(new { message = "Category and Value are required" });
var existing = await _db.DropdownOptions
.AnyAsync(d => d.Category == model.Category && d.Value == model.Value && d.ParentValue == model.ParentValue);
if (existing)
return BadRequest(new { message = "This option already exists" });
model.Id = 0;
model.IsActive = true;
_db.DropdownOptions.Add(model);
await _db.SaveChangesAsync();
return Ok(model);
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [FromBody] DropdownOption model)
{
var item = await _db.DropdownOptions.FindAsync(id);
if (item == null)
return NotFound(new { message = "Option not found" });
item.Value = model.Value;
item.ParentValue = model.ParentValue;
item.SortOrder = model.SortOrder;
item.IsActive = model.IsActive;
await _db.SaveChangesAsync();
return Ok(item);
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var item = await _db.DropdownOptions.FindAsync(id);
if (item == null)
return NotFound(new { message = "Option not found" });
_db.DropdownOptions.Remove(item);
await _db.SaveChangesAsync();
return Ok(new { message = "Deleted" });
}
}
}

View file

@ -213,6 +213,13 @@ namespace Api.SeaHavenIndustries.Controllers
LogChange("Priority", workOrder.Priority, model.Priority);
LogChange("LocationId", workOrder.LocationId?.ToString(), model.LocationId?.ToString());
LogChange("Status", workOrder.Status, model.Status);
LogChange("Problem", workOrder.Problem, model.Problem);
LogChange("Trade", workOrder.Trade, model.Trade);
LogChange("SubTrade", workOrder.SubTrade, model.SubTrade);
LogChange("VendorNTE", workOrder.VendorNTE?.ToString("F2"), model.VendorNTE?.ToString("F2"));
LogChange("ScheduledDate", workOrder.ScheduledDate?.ToString("yyyy-MM-dd"), model.ScheduledDate?.ToString("yyyy-MM-dd"));
LogChange("CompletedDate", workOrder.CompletedDate?.ToString("yyyy-MM-dd"), model.CompletedDate?.ToString("yyyy-MM-dd"));
LogChange("Source", workOrder.Source, model.Source);
workOrder.WorkerOrderNumber = model.WorkerOrderNumber;
workOrder.WorkerOrderTitle = model.WorkerOrderTitle;
@ -224,6 +231,13 @@ namespace Api.SeaHavenIndustries.Controllers
workOrder.Priority = model.Priority;
workOrder.LocationId = model.LocationId;
workOrder.Status = model.Status;
workOrder.Problem = model.Problem;
workOrder.Trade = model.Trade;
workOrder.SubTrade = model.SubTrade;
workOrder.VendorNTE = model.VendorNTE;
workOrder.ScheduledDate = model.ScheduledDate;
workOrder.CompletedDate = model.CompletedDate;
workOrder.Source = model.Source;
workOrder.BeforPhotoIssue = model.BeforPhotoIssue;
workOrder.AfterPhotoIssue = model.AfterPhotoIssue;
workOrder.SignOffName = model.SignOffName;
@ -588,11 +602,18 @@ namespace Api.SeaHavenIndustries.Controllers
s.Description,
s.CreatedDate,
s.DueDate,
s.ScheduledDate,
s.CompletedDate,
s.Status,
assignee = s.AssignToUser != null ? s.AssignToUser.FirstName : "",
location = s.Locations != null ? s.Locations.Name : "",
locationId = s.LocationId,
priority = s.Priority,
s.Problem,
s.Trade,
s.SubTrade,
s.VendorNTE,
s.Source,
s.PO,
s.TT,
poc = s.WorkOrderContacts.Select(c => new { c.Id, name = c.POC != null ? (c.POC.FirstName + " " + c.POC.LastName).Trim() : "", email = c.POC != null ? c.POC.Email : "", phone = c.POC != null ? c.POC.PhoneNumber : "" }),

View file

@ -49,7 +49,13 @@ namespace Api.SeaHavenIndustries.DTOs
public string? Priority { get; set; }
public string? Status { get; set; }
public int? LocationId { get; set; }
//public int? CategoryId { get; set; }
public string? Problem { get; set; }
public string? Trade { get; set; }
public string? SubTrade { get; set; }
public decimal? VendorNTE { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? CompletedDate { get; set; }
public string? Source { get; set; }
public List<IFormFile>? Attachments { get; set; }
public string? BeforPhotoIssue { get; set; }

View file

@ -128,6 +128,30 @@ if (app.Environment.IsDevelopment())
if (result.Succeeded)
await userManager.AddToRoleAsync(admin, "Admin");
}
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
if (!db.DropdownOptions.Any())
{
var trades = new[] { "Plumbing", "Electrical", "HVAC", "Fire/Life Safety", "General", "Roofing", "Janitorial", "Painting", "Locksmith", "Carpentry" };
for (int i = 0; i < trades.Length; i++)
db.DropdownOptions.Add(new DropdownOption { Category = "Trade", Value = trades[i], SortOrder = i });
var subTrades = new (string trade, string sub)[] {
("Plumbing", "Drain Cleaning"), ("Plumbing", "Pipe Repair"), ("Plumbing", "Fixture Install"), ("Plumbing", "Backflow Testing"),
("Electrical", "Lighting"), ("Electrical", "Panel/Breaker"), ("Electrical", "Wiring"), ("Electrical", "Generator"),
("HVAC", "AC Repair"), ("HVAC", "Heating"), ("HVAC", "Ductwork"), ("HVAC", "Controls/BMS"),
("Fire/Life Safety", "Sprinkler"), ("Fire/Life Safety", "Fire Alarm"), ("Fire/Life Safety", "Extinguisher"), ("Fire/Life Safety", "Fire Pump"),
("General", "Doors"), ("General", "Flooring"), ("General", "Drywall"), ("General", "Concrete"),
};
for (int i = 0; i < subTrades.Length; i++)
db.DropdownOptions.Add(new DropdownOption { Category = "SubTrade", Value = subTrades[i].sub, ParentValue = subTrades[i].trade, SortOrder = i });
var problems = new[] { "Leak", "No Power", "Not Cooling", "Not Heating", "Clogged", "Broken/Damaged", "Alarm Fault", "Preventive Maintenance", "Inspection", "Installation", "Other" };
for (int i = 0; i < problems.Length; i++)
db.DropdownOptions.Add(new DropdownOption { Category = "Problem", Value = problems[i], SortOrder = i });
await db.SaveChangesAsync();
}
}
app.Run();

View file

@ -38,6 +38,7 @@ namespace Data.SeaHavenIndustries
public DbSet<WorkOrderCategories> workOrderCategories { get; set; }
public DbSet<WorkOrderAttachments> workOrderAttachments { get; set; }
public DbSet<WorkOrderAuditLog> WorkOrderAuditLogs { get; set; }
public DbSet<DropdownOption> DropdownOptions { 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,109 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddDropdownOptionsAndWOFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "CompletedDate",
table: "workOrders",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Problem",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "ScheduledDate",
table: "workOrders",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Source",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SubTrade",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Trade",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "VendorNTE",
table: "workOrders",
type: "decimal(18,2)",
nullable: true);
migrationBuilder.CreateTable(
name: "DropdownOptions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Category = table.Column<string>(type: "nvarchar(max)", nullable: false),
Value = table.Column<string>(type: "nvarchar(max)", nullable: false),
ParentValue = table.Column<string>(type: "nvarchar(max)", nullable: true),
SortOrder = table.Column<int>(type: "int", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DropdownOptions", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DropdownOptions");
migrationBuilder.DropColumn(
name: "CompletedDate",
table: "workOrders");
migrationBuilder.DropColumn(
name: "Problem",
table: "workOrders");
migrationBuilder.DropColumn(
name: "ScheduledDate",
table: "workOrders");
migrationBuilder.DropColumn(
name: "Source",
table: "workOrders");
migrationBuilder.DropColumn(
name: "SubTrade",
table: "workOrders");
migrationBuilder.DropColumn(
name: "Trade",
table: "workOrders");
migrationBuilder.DropColumn(
name: "VendorNTE",
table: "workOrders");
}
}
}

View file

@ -627,6 +627,36 @@ namespace Data.SeaHavenIndustries.Migrations
b.ToTable("Departments");
});
modelBuilder.Entity("Data.SeaHavenIndustries.DropdownOption", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Category")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("ParentValue")
.HasColumnType("nvarchar(max)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("DropdownOptions");
});
modelBuilder.Entity("Data.SeaHavenIndustries.Employee", b =>
{
b.Property<int>("Id")
@ -1408,6 +1438,9 @@ namespace Data.SeaHavenIndustries.Migrations
b.Property<string>("Building")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("CompletedDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("CreatedDate")
.HasColumnType("datetime2");
@ -1453,6 +1486,12 @@ namespace Data.SeaHavenIndustries.Migrations
b.Property<string>("Priority")
.HasColumnType("nvarchar(max)");
b.Property<string>("Problem")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("ScheduledDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("ScheduledStart")
.HasColumnType("datetime2");
@ -1471,15 +1510,27 @@ namespace Data.SeaHavenIndustries.Migrations
b.Property<string>("SiteCode")
.HasColumnType("nvarchar(max)");
b.Property<string>("Source")
.HasColumnType("nvarchar(max)");
b.Property<string>("SourceEmailS3Key")
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.HasColumnType("nvarchar(max)");
b.Property<string>("SubTrade")
.HasColumnType("nvarchar(max)");
b.Property<string>("TT")
.HasColumnType("nvarchar(max)");
b.Property<string>("Trade")
.HasColumnType("nvarchar(max)");
b.Property<decimal?>("VendorNTE")
.HasColumnType("decimal(18,2)");
b.Property<string>("WorkerOrderNumber")
.HasColumnType("nvarchar(max)");

View file

@ -0,0 +1,12 @@
namespace Data.SeaHavenIndustries
{
public class DropdownOption
{
public int Id { get; set; }
public string Category { get; set; } = "";
public string Value { get; set; } = "";
public string? ParentValue { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; } = true;
}
}

View file

@ -18,6 +18,13 @@ namespace Data.SeaHavenIndustries
public DateTime? DateReported { get; set; }
public DateTime? ScheduledStart { get; set; }
public string? SourceEmailS3Key { get; set; }
public string? Problem { get; set; }
public string? Trade { get; set; }
public string? SubTrade { get; set; }
public decimal? VendorNTE { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? CompletedDate { get; set; }
public string? Source { get; set; }
public bool? istemplate { get; set; }
public string? PO { get; set; }
public string? TT { get; set; }