Add comment types, audit logging, and WorkOrderAuditLog table

- Add CommentType field to Comments (customer, vendor, internal)
- Add Status field to EditWorkorder_DTO
- Create WorkOrderAuditLog model tracking field-level changes
- Log all field changes on work order edit and status change
- Include commentType and auditLog in GetWorkorderById response
- Set CommentType=customer on synced comments from DynamoDB
- Add BackfillCommentTypes endpoint for existing data
This commit is contained in:
Adam Moussa 2026-04-16 18:09:57 -04:00
parent c69cef7fa9
commit e1f57d3fb2
10 changed files with 2492 additions and 6 deletions

View file

@ -176,6 +176,7 @@ namespace Api.SeaHavenIndustries.Controllers
Commenttext = GetString(item, "text"),
Commenter = GetString(item, "commenter"),
RecordType = GetString(item, "record_type"),
CommentType = "customer",
CreatedDate = ParseDate(GetString(item, "created_at")) ?? DateTime.UtcNow,
};
@ -196,6 +197,14 @@ namespace Api.SeaHavenIndustries.Controllers
return Ok(new { synced, created, skipped });
}
[HttpPost("BackfillCommentTypes")]
public async Task<IActionResult> BackfillCommentTypes()
{
var updated = await _db.Database.ExecuteSqlRawAsync(
"UPDATE Comments SET CommentType = 'customer' WHERE ExternalCommentId IS NOT NULL AND (CommentType IS NULL OR CommentType = '')");
return Ok(new { updated });
}
[HttpPost("All")]
public async Task<IActionResult> SyncAll()
{

View file

@ -184,6 +184,35 @@ namespace Api.SeaHavenIndustries.Controllers
var workOrder = _db.workOrders.Where(w => w.Id == model.Id).FirstOrDefault();
if (workOrder == null)
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var now = DateTime.UtcNow;
void LogChange(string field, string? oldVal, string? newVal)
{
if (oldVal == newVal) return;
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = workOrder.Id,
UserId = userId,
FieldName = field,
OldValue = oldVal ?? "",
NewValue = newVal ?? "",
Action = "edit",
CreatedAt = now
});
}
LogChange("WorkOrderNumber", workOrder.WorkerOrderNumber, model.WorkerOrderNumber);
LogChange("Title", workOrder.WorkerOrderTitle, model.WorkerOrderTitle);
LogChange("Description", workOrder.Description, model.Description);
LogChange("TT", workOrder.TT, model.TT);
LogChange("PO", workOrder.PO, model.PO);
LogChange("AssignTo", workOrder.AssignTo, model.AssignTo);
LogChange("DueDate", workOrder.DueDate?.ToString("yyyy-MM-dd"), model.DueDate?.ToString("yyyy-MM-dd"));
LogChange("Priority", workOrder.Priority, model.Priority);
LogChange("LocationId", workOrder.LocationId?.ToString(), model.LocationId?.ToString());
LogChange("Status", workOrder.Status, model.Status);
workOrder.WorkerOrderNumber = model.WorkerOrderNumber;
workOrder.WorkerOrderTitle = model.WorkerOrderTitle;
workOrder.Description = model.Description;
@ -192,10 +221,8 @@ namespace Api.SeaHavenIndustries.Controllers
workOrder.AssignTo = model.AssignTo;
workOrder.DueDate = model.DueDate;
workOrder.Priority = model.Priority;
//workOrder.CategoryId = model.CategoryId;
workOrder.LocationId = model.LocationId;
workOrder.Status = "Open";
//workOrder.istemplate = false;
workOrder.Status = model.Status;
workOrder.BeforPhotoIssue = model.BeforPhotoIssue;
workOrder.AfterPhotoIssue = model.AfterPhotoIssue;
workOrder.SignOffName = model.SignOffName;
@ -575,8 +602,20 @@ namespace Api.SeaHavenIndustries.Controllers
s.SignOffName,
s.SignOffAttachment,
s.SignOffSignature,
Comments = s.Comments.Select(c => new { c.Id, c.CreatedDate, c.Commenttext, c.Commenter, c.RecordType, c.Documents, userName = c.ApplicationUser != null ? c.ApplicationUser.FirstName : "" }),
quotes = s.Quotes.Select(q => new { code = q.QuotId, q.Id })
Comments = s.Comments.Select(c => new { c.Id, c.CreatedDate, c.Commenttext, c.Commenter, c.RecordType, c.CommentType, c.Documents, userName = c.ApplicationUser != null ? c.ApplicationUser.FirstName : "" }),
quotes = s.Quotes.Select(q => new { code = q.QuotId, q.Id }),
auditLog = _db.WorkOrderAuditLogs
.Where(a => a.WorkOrderId == s.Id)
.OrderByDescending(a => a.CreatedAt)
.Select(a => new {
a.Id,
a.FieldName,
a.OldValue,
a.NewValue,
a.Action,
a.CreatedAt,
userName = a.User != null ? a.User.FirstName + " " + a.User.LastName : ""
}).ToList()
}).FirstOrDefaultAsync();
if (exist == null)
{
@ -720,6 +759,19 @@ namespace Api.SeaHavenIndustries.Controllers
var data = await _db.workOrders.Where(w => w.Id == id).FirstOrDefaultAsync();
if (data == null)
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
if (data.Status != status)
{
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = id,
UserId = User.FindFirstValue(ClaimTypes.NameIdentifier),
FieldName = "Status",
OldValue = data.Status ?? "",
NewValue = status,
Action = "status_change",
CreatedAt = DateTime.UtcNow
});
}
data.Status = status;
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = data });
@ -742,6 +794,7 @@ namespace Api.SeaHavenIndustries.Controllers
comments.UserId = userId;
comments.WorkerOrderId = model.WorkorderId;
comments.Commenttext = model.Text;
comments.CommentType = model.CommentType ?? "vendor";
if (model.Document != null)
{
string fileName = ContentDispositionHeaderValue.Parse(model.Document.ContentDisposition).FileName.Trim('"');

View file

@ -7,5 +7,6 @@
public int WorkorderId { get; set; }
public DateTime CreatedDate { get; set; }
public string UserId { get; set; }
public string? CommentType { get; set; }
}
}

View file

@ -44,10 +44,10 @@ namespace Api.SeaHavenIndustries.DTOs
public string? Description { get; set; }
public string? PO { get; set; }
public string? TT { get; set; }
//public int? ContactId { get; set; }
public string? AssignTo { get; set; }
public DateTime? DueDate { get; set; }
public string? Priority { get; set; }
public string? Status { get; set; }
public int? LocationId { get; set; }
//public int? CategoryId { get; set; }

View file

@ -37,6 +37,7 @@ namespace Data.SeaHavenIndustries
public DbSet<WorkOrderContacts> WorkOrderContacts { get; set; }
public DbSet<WorkOrderCategories> workOrderCategories { get; set; }
public DbSet<WorkOrderAttachments> workOrderAttachments { get; set; }
public DbSet<WorkOrderAuditLog> WorkOrderAuditLogs { 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,73 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddCommentTypeAndAuditLog : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CommentType",
table: "Comments",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.CreateTable(
name: "WorkOrderAuditLogs",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
WorkOrderId = table.Column<int>(type: "int", nullable: false),
UserId = table.Column<string>(type: "nvarchar(450)", nullable: true),
FieldName = table.Column<string>(type: "nvarchar(max)", nullable: true),
OldValue = table.Column<string>(type: "nvarchar(max)", nullable: true),
NewValue = table.Column<string>(type: "nvarchar(max)", nullable: true),
Action = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WorkOrderAuditLogs", x => x.Id);
table.ForeignKey(
name: "FK_WorkOrderAuditLogs_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_WorkOrderAuditLogs_workOrders_WorkOrderId",
column: x => x.WorkOrderId,
principalTable: "workOrders",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_WorkOrderAuditLogs_UserId",
table: "WorkOrderAuditLogs",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_WorkOrderAuditLogs_WorkOrderId",
table: "WorkOrderAuditLogs",
column: "WorkOrderId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WorkOrderAuditLogs");
migrationBuilder.DropColumn(
name: "CommentType",
table: "Comments");
}
}
}

View file

@ -376,6 +376,9 @@ namespace Data.SeaHavenIndustries.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("CommentType")
.HasColumnType("nvarchar(max)");
b.Property<string>("Commenter")
.HasColumnType("nvarchar(max)");
@ -1537,6 +1540,44 @@ namespace Data.SeaHavenIndustries.Migrations
b.ToTable("workOrderAttachments");
});
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderAuditLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Action")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("FieldName")
.HasColumnType("nvarchar(max)");
b.Property<string>("NewValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("OldValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<int>("WorkOrderId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("WorkOrderId");
b.ToTable("WorkOrderAuditLogs");
});
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderCategories", b =>
{
b.Property<int>("Id")
@ -2037,6 +2078,24 @@ namespace Data.SeaHavenIndustries.Migrations
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")

View file

@ -9,6 +9,7 @@ namespace Data.SeaHavenIndustries
public string? Commenttext { get; set; }
public string? Commenter { get; set; }
public string? RecordType { get; set; }
public string? CommentType { get; set; }
public string? ExternalCommentId { get; set; }
public string? Documents { get; set; }
public string? UserId { get; set; }

View file

@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.SeaHavenIndustries
{
public class WorkOrderAuditLog
{
public int Id { get; set; }
public int WorkOrderId { get; set; }
[ForeignKey(nameof(WorkOrderId))]
public virtual WorkOrder? WorkOrder { get; set; }
public string? UserId { get; set; }
[ForeignKey(nameof(UserId))]
public virtual ApplicationUser? User { get; set; }
public string? FieldName { get; set; }
public string? OldValue { get; set; }
public string? NewValue { get; set; }
public string? Action { get; set; }
public DateTime CreatedAt { get; set; }
}
}