mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-03 12:43:33 +00:00
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:
parent
c69cef7fa9
commit
e1f57d3fb2
10 changed files with 2492 additions and 6 deletions
|
|
@ -176,6 +176,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
Commenttext = GetString(item, "text"),
|
Commenttext = GetString(item, "text"),
|
||||||
Commenter = GetString(item, "commenter"),
|
Commenter = GetString(item, "commenter"),
|
||||||
RecordType = GetString(item, "record_type"),
|
RecordType = GetString(item, "record_type"),
|
||||||
|
CommentType = "customer",
|
||||||
CreatedDate = ParseDate(GetString(item, "created_at")) ?? DateTime.UtcNow,
|
CreatedDate = ParseDate(GetString(item, "created_at")) ?? DateTime.UtcNow,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -196,6 +197,14 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
return Ok(new { synced, created, skipped });
|
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")]
|
[HttpPost("All")]
|
||||||
public async Task<IActionResult> SyncAll()
|
public async Task<IActionResult> SyncAll()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,35 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
var workOrder = _db.workOrders.Where(w => w.Id == model.Id).FirstOrDefault();
|
var workOrder = _db.workOrders.Where(w => w.Id == model.Id).FirstOrDefault();
|
||||||
if (workOrder == null)
|
if (workOrder == null)
|
||||||
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
|
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.WorkerOrderNumber = model.WorkerOrderNumber;
|
||||||
workOrder.WorkerOrderTitle = model.WorkerOrderTitle;
|
workOrder.WorkerOrderTitle = model.WorkerOrderTitle;
|
||||||
workOrder.Description = model.Description;
|
workOrder.Description = model.Description;
|
||||||
|
|
@ -192,10 +221,8 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
workOrder.AssignTo = model.AssignTo;
|
workOrder.AssignTo = model.AssignTo;
|
||||||
workOrder.DueDate = model.DueDate;
|
workOrder.DueDate = model.DueDate;
|
||||||
workOrder.Priority = model.Priority;
|
workOrder.Priority = model.Priority;
|
||||||
//workOrder.CategoryId = model.CategoryId;
|
|
||||||
workOrder.LocationId = model.LocationId;
|
workOrder.LocationId = model.LocationId;
|
||||||
workOrder.Status = "Open";
|
workOrder.Status = model.Status;
|
||||||
//workOrder.istemplate = false;
|
|
||||||
workOrder.BeforPhotoIssue = model.BeforPhotoIssue;
|
workOrder.BeforPhotoIssue = model.BeforPhotoIssue;
|
||||||
workOrder.AfterPhotoIssue = model.AfterPhotoIssue;
|
workOrder.AfterPhotoIssue = model.AfterPhotoIssue;
|
||||||
workOrder.SignOffName = model.SignOffName;
|
workOrder.SignOffName = model.SignOffName;
|
||||||
|
|
@ -575,8 +602,20 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
s.SignOffName,
|
s.SignOffName,
|
||||||
s.SignOffAttachment,
|
s.SignOffAttachment,
|
||||||
s.SignOffSignature,
|
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 : "" }),
|
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 })
|
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();
|
}).FirstOrDefaultAsync();
|
||||||
if (exist == null)
|
if (exist == null)
|
||||||
{
|
{
|
||||||
|
|
@ -720,6 +759,19 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
var data = await _db.workOrders.Where(w => w.Id == id).FirstOrDefaultAsync();
|
var data = await _db.workOrders.Where(w => w.Id == id).FirstOrDefaultAsync();
|
||||||
if (data == null)
|
if (data == null)
|
||||||
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
|
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;
|
data.Status = status;
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = data });
|
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = data });
|
||||||
|
|
@ -742,6 +794,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
comments.UserId = userId;
|
comments.UserId = userId;
|
||||||
comments.WorkerOrderId = model.WorkorderId;
|
comments.WorkerOrderId = model.WorkorderId;
|
||||||
comments.Commenttext = model.Text;
|
comments.Commenttext = model.Text;
|
||||||
|
comments.CommentType = model.CommentType ?? "vendor";
|
||||||
if (model.Document != null)
|
if (model.Document != null)
|
||||||
{
|
{
|
||||||
string fileName = ContentDispositionHeaderValue.Parse(model.Document.ContentDisposition).FileName.Trim('"');
|
string fileName = ContentDispositionHeaderValue.Parse(model.Document.ContentDisposition).FileName.Trim('"');
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@
|
||||||
public int WorkorderId { get; set; }
|
public int WorkorderId { get; set; }
|
||||||
public DateTime CreatedDate { get; set; }
|
public DateTime CreatedDate { get; set; }
|
||||||
public string UserId { get; set; }
|
public string UserId { get; set; }
|
||||||
|
public string? CommentType { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,10 @@ namespace Api.SeaHavenIndustries.DTOs
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public string? PO { get; set; }
|
public string? PO { get; set; }
|
||||||
public string? TT { get; set; }
|
public string? TT { get; set; }
|
||||||
//public int? ContactId { get; set; }
|
|
||||||
public string? AssignTo { get; set; }
|
public string? AssignTo { get; set; }
|
||||||
public DateTime? DueDate { get; set; }
|
public DateTime? DueDate { get; set; }
|
||||||
public string? Priority { get; set; }
|
public string? Priority { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
public int? LocationId { get; set; }
|
public int? LocationId { get; set; }
|
||||||
//public int? CategoryId { get; set; }
|
//public int? CategoryId { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ namespace Data.SeaHavenIndustries
|
||||||
public DbSet<WorkOrderContacts> WorkOrderContacts { get; set; }
|
public DbSet<WorkOrderContacts> WorkOrderContacts { get; set; }
|
||||||
public DbSet<WorkOrderCategories> workOrderCategories { get; set; }
|
public DbSet<WorkOrderCategories> workOrderCategories { get; set; }
|
||||||
public DbSet<WorkOrderAttachments> workOrderAttachments { get; set; }
|
public DbSet<WorkOrderAttachments> workOrderAttachments { get; set; }
|
||||||
|
public DbSet<WorkOrderAuditLog> WorkOrderAuditLogs { get; set; }
|
||||||
|
|
||||||
// New DbSets for added entities
|
// New DbSets for added entities
|
||||||
public DbSet<Accounts> Accounts { get; set; }
|
public DbSet<Accounts> Accounts { get; set; }
|
||||||
|
|
|
||||||
2269
Data.SeaHavenIndustries/Migrations/20260416220656_AddCommentTypeAndAuditLog.Designer.cs
generated
Normal file
2269
Data.SeaHavenIndustries/Migrations/20260416220656_AddCommentTypeAndAuditLog.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -376,6 +376,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("CommentType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<string>("Commenter")
|
b.Property<string>("Commenter")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
|
@ -1537,6 +1540,44 @@ namespace Data.SeaHavenIndustries.Migrations
|
||||||
b.ToTable("workOrderAttachments");
|
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 =>
|
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderCategories", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|
@ -2037,6 +2078,24 @@ namespace Data.SeaHavenIndustries.Migrations
|
||||||
b.Navigation("WorkOrder");
|
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 =>
|
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrderCategories", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Data.SeaHavenIndustries.Category", "Category")
|
b.HasOne("Data.SeaHavenIndustries.Category", "Category")
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ namespace Data.SeaHavenIndustries
|
||||||
public string? Commenttext { get; set; }
|
public string? Commenttext { get; set; }
|
||||||
public string? Commenter { get; set; }
|
public string? Commenter { get; set; }
|
||||||
public string? RecordType { get; set; }
|
public string? RecordType { get; set; }
|
||||||
|
public string? CommentType { get; set; }
|
||||||
public string? ExternalCommentId { get; set; }
|
public string? ExternalCommentId { get; set; }
|
||||||
public string? Documents { get; set; }
|
public string? Documents { get; set; }
|
||||||
public string? UserId { get; set; }
|
public string? UserId { get; set; }
|
||||||
|
|
|
||||||
20
Data.SeaHavenIndustries/Models/WorkOrderAuditLog.cs
Normal file
20
Data.SeaHavenIndustries/Models/WorkOrderAuditLog.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue