mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
feat(work-orders): add Phase 6 slide-over detail comments audit and media API
This commit is contained in:
parent
0ce008e5eb
commit
af84001bf1
18 changed files with 1313 additions and 4 deletions
|
|
@ -48,6 +48,10 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
private readonly IWorkOrderBoardCreateService _boardCreateService;
|
||||
private readonly IWorkOrderBoardCancelService _boardCancelService;
|
||||
private readonly IWorkOrderAdvancedSearchService _advancedSearchService;
|
||||
private readonly IWorkOrderDetailService _workOrderDetailService;
|
||||
private readonly IWorkOrderCommentService _workOrderCommentService;
|
||||
private readonly IWorkOrderCompletionService _workOrderCompletionService;
|
||||
private readonly IWorkOrderMediaService _workOrderMediaService;
|
||||
|
||||
public WorkOrderController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
|
|
@ -69,7 +73,11 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
IWorkOrderBoardUpdateService boardUpdateService,
|
||||
IWorkOrderBoardCreateService boardCreateService,
|
||||
IWorkOrderBoardCancelService boardCancelService,
|
||||
IWorkOrderAdvancedSearchService advancedSearchService)
|
||||
IWorkOrderAdvancedSearchService advancedSearchService,
|
||||
IWorkOrderDetailService workOrderDetailService,
|
||||
IWorkOrderCommentService workOrderCommentService,
|
||||
IWorkOrderCompletionService workOrderCompletionService,
|
||||
IWorkOrderMediaService workOrderMediaService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_workOrderService = workOrderService;
|
||||
|
|
@ -91,6 +99,10 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
_boardCreateService = boardCreateService;
|
||||
_boardCancelService = boardCancelService;
|
||||
_advancedSearchService = advancedSearchService;
|
||||
_workOrderDetailService = workOrderDetailService;
|
||||
_workOrderCommentService = workOrderCommentService;
|
||||
_workOrderCompletionService = workOrderCompletionService;
|
||||
_workOrderMediaService = workOrderMediaService;
|
||||
}
|
||||
|
||||
[HttpGet("board")]
|
||||
|
|
@ -220,6 +232,216 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/detail")]
|
||||
public async Task<IActionResult> GetDetail(int id)
|
||||
{
|
||||
var detail = await _workOrderDetailService.GetDetailAsync(id);
|
||||
if (detail == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Work order not found." });
|
||||
return Ok(detail);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/audit")]
|
||||
public async Task<IActionResult> GetAudit(int id, [FromQuery] int limit = 50)
|
||||
{
|
||||
var audit = await _workOrderDetailService.GetAuditAsync(id, limit);
|
||||
if (audit == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Work order not found." });
|
||||
return Ok(audit);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/comments")]
|
||||
public async Task<IActionResult> GetBoardComments(int id)
|
||||
{
|
||||
var comments = await _workOrderCommentService.GetCommentsAsync(id);
|
||||
if (comments == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Work order not found." });
|
||||
return Ok(comments);
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/comments")]
|
||||
public async Task<IActionResult> AddBoardComment(int id, [FromBody] WorkOrderCommentCreateDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var comment = await _workOrderCommentService.AddCommentAsync(id, request, actorId);
|
||||
return Ok(comment);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("completion-templates")]
|
||||
public async Task<IActionResult> GetCompletionTemplates(
|
||||
[FromQuery] string? serviceKey = null,
|
||||
[FromQuery] WorkOrderType? workOrderType = null)
|
||||
{
|
||||
var templates = await _workOrderCompletionService.GetTemplatesAsync(serviceKey, workOrderType);
|
||||
return Ok(templates);
|
||||
}
|
||||
|
||||
[HttpGet("completion-templates/{id:int}")]
|
||||
public async Task<IActionResult> GetCompletionTemplate(int id)
|
||||
{
|
||||
var template = await _workOrderCompletionService.GetTemplateByIdAsync(id);
|
||||
if (template == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Template not found." });
|
||||
return Ok(template);
|
||||
}
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost("completion-templates")]
|
||||
public async Task<IActionResult> CreateCompletionTemplate([FromBody] CompletionDocTemplateCreateDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var created = await _workOrderCompletionService.CreateTemplateAsync(request);
|
||||
return Ok(created);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPut("completion-templates/{id:int}")]
|
||||
public async Task<IActionResult> UpdateCompletionTemplate(int id, [FromBody] CompletionDocTemplateCreateDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var updated = await _workOrderCompletionService.UpdateTemplateAsync(id, request);
|
||||
return Ok(updated);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("completion-templates/{id:int}")]
|
||||
public async Task<IActionResult> DeleteCompletionTemplate(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _workOrderCompletionService.DeleteTemplateAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/completion-doc")]
|
||||
[RequestSizeLimit(30_000_000)]
|
||||
public async Task<IActionResult> UploadCompletionDoc(
|
||||
int id,
|
||||
[FromForm] IFormFile file,
|
||||
[FromForm] string? signOffName = null,
|
||||
[FromForm] string? signOffSignature = null,
|
||||
[FromForm] string? workOrderVersion = null)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
return BadRequest(new Response { Status = "Error", Message = "file is required." });
|
||||
|
||||
try
|
||||
{
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var fileUrl = await SaveFileAsync(file);
|
||||
var result = await _workOrderCompletionService.UploadCompletionDocAsync(
|
||||
id,
|
||||
new WorkOrderCompletionDocUploadDto
|
||||
{
|
||||
SignOffName = signOffName,
|
||||
SignOffSignature = signOffSignature,
|
||||
WorkOrderVersion = workOrderVersion
|
||||
},
|
||||
fileUrl,
|
||||
actorId);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code is "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code is "ConcurrencyConflict")
|
||||
{
|
||||
return Conflict(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/media")]
|
||||
public async Task<IActionResult> GetMedia(int id)
|
||||
{
|
||||
var media = await _workOrderMediaService.GetMediaAsync(id);
|
||||
if (media == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Work order not found." });
|
||||
return Ok(media);
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/media")]
|
||||
[RequestSizeLimit(30_000_000)]
|
||||
public async Task<IActionResult> AddMedia(
|
||||
int id,
|
||||
[FromForm] WorkOrderMediaCategory category,
|
||||
[FromForm] IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
return BadRequest(new Response { Status = "Error", Message = "file is required." });
|
||||
|
||||
try
|
||||
{
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var fileUrl = await SaveFileAsync(file);
|
||||
var media = await _workOrderMediaService.AddMediaAsync(id, category, fileUrl, actorId);
|
||||
return Ok(media);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}/media/{mediaId:int}")]
|
||||
public async Task<IActionResult> DeleteMedia(int id, int mediaId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
await _workOrderMediaService.DeleteMediaAsync(id, mediaId, actorId);
|
||||
return NoContent();
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("AddWorkorder")]
|
||||
public async Task<IActionResult> Addworkorder([FromForm] Workorder_DTO model)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
{
|
||||
public class CompletionDocTemplateDataService : ICompletionDocTemplateDataService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public CompletionDocTemplateDataService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CompletionDocTemplateRow>> GetActiveAsync(string? serviceKey, WorkOrderType? workOrderType)
|
||||
{
|
||||
var query = _context.CompletionDocTemplates
|
||||
.AsNoTracking()
|
||||
.Where(t => t.IsActive && t.IsDeleted != true);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(serviceKey))
|
||||
query = query.Where(t => t.ServiceKey == serviceKey);
|
||||
|
||||
if (workOrderType.HasValue)
|
||||
query = query.Where(t => t.WorkOrderType == null || t.WorkOrderType == workOrderType);
|
||||
|
||||
return await query
|
||||
.OrderBy(t => t.Name)
|
||||
.Select(MapRow)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<CompletionDocTemplateRow?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.CompletionDocTemplates
|
||||
.AsNoTracking()
|
||||
.Where(t => t.Id == id && t.IsDeleted != true)
|
||||
.Select(MapRow)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<CompletionDocTemplateRow?> ResolveForWorkOrderAsync(string? trade, WorkOrderType? workOrderType)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(trade))
|
||||
{
|
||||
var byTrade = await _context.CompletionDocTemplates
|
||||
.AsNoTracking()
|
||||
.Where(t => t.IsActive && t.IsDeleted != true && t.ServiceKey == trade)
|
||||
.OrderBy(t => t.WorkOrderType == workOrderType ? 0 : 1)
|
||||
.ThenBy(t => t.Name)
|
||||
.ThenBy(t => t.Id)
|
||||
.Select(MapRow)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (byTrade != null)
|
||||
return byTrade;
|
||||
}
|
||||
|
||||
if (!workOrderType.HasValue)
|
||||
return null;
|
||||
|
||||
return await _context.CompletionDocTemplates
|
||||
.AsNoTracking()
|
||||
.Where(t => t.IsActive && t.IsDeleted != true && t.WorkOrderType == workOrderType)
|
||||
.OrderBy(t => t.Name)
|
||||
.ThenBy(t => t.Id)
|
||||
.Select(MapRow)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<CompletionDocTemplate> CreateAsync(CompletionDocTemplate template)
|
||||
{
|
||||
template.CreatedDate = DateTime.UtcNow;
|
||||
await _context.CompletionDocTemplates.AddAsync(template);
|
||||
await _context.SaveChangesAsync();
|
||||
return template;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(CompletionDocTemplate template)
|
||||
{
|
||||
template.LastModificationTime = DateTime.UtcNow;
|
||||
_context.CompletionDocTemplates.Update(template);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id)
|
||||
{
|
||||
var entity = await _context.CompletionDocTemplates.FindAsync(id);
|
||||
if (entity == null)
|
||||
return false;
|
||||
|
||||
entity.IsDeleted = true;
|
||||
entity.DeletionTime = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly System.Linq.Expressions.Expression<Func<CompletionDocTemplate, CompletionDocTemplateRow>> MapRow =
|
||||
t => new CompletionDocTemplateRow(t.Id, t.Name, t.ServiceKey, t.WorkOrderType, t.TemplateUrl, t.IsActive);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Helpers;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
{
|
||||
public class WorkOrderDetailDataService : IWorkOrderDetailDataService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public WorkOrderDetailDataService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public Task<bool> ExistsAsync(int workOrderId)
|
||||
=> WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking())
|
||||
.AnyAsync(w => w.Id == workOrderId);
|
||||
|
||||
public async Task<WorkOrderDetailExtendedFields?> GetExtendedFieldsAsync(int workOrderId)
|
||||
{
|
||||
return await WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking())
|
||||
.Where(w => w.Id == workOrderId)
|
||||
.Select(w => new WorkOrderDetailExtendedFields(
|
||||
w.Description,
|
||||
w.Trade,
|
||||
w.Problem,
|
||||
w.SubTrade,
|
||||
w.OriginalWeek,
|
||||
w.OriginalDate,
|
||||
w.CompletedDate,
|
||||
w.SignOffName,
|
||||
w.SignOffAttachment,
|
||||
w.SignOffSignature))
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Comments>> GetCommentsAsync(int workOrderId)
|
||||
{
|
||||
return await _context.Comments
|
||||
.AsNoTracking()
|
||||
.Where(c => c.WorkerOrderId == workOrderId && c.IsDeleted != true)
|
||||
.OrderByDescending(c => c.CreatedDate)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WorkOrderAuditLog>> GetAuditLogsAsync(int workOrderId, int? limit = null)
|
||||
{
|
||||
var query = _context.WorkOrderAuditLogs
|
||||
.AsNoTracking()
|
||||
.Where(a => a.WorkOrderId == workOrderId)
|
||||
.OrderByDescending(a => a.CreatedAt);
|
||||
|
||||
if (limit.HasValue)
|
||||
return await query.Take(limit.Value).ToListAsync();
|
||||
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WorkOrderAttachments>> GetAttachmentsAsync(int workOrderId)
|
||||
{
|
||||
return await _context.workOrderAttachments
|
||||
.AsNoTracking()
|
||||
.Where(a => a.WorkorderId == workOrderId && a.IsDeleted != true)
|
||||
.OrderByDescending(a => a.CreatedDate)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DispatchSignoffRow>> GetDispatchSignoffsAsync(int workOrderId)
|
||||
{
|
||||
return await _context.DispatchSignoffs
|
||||
.AsNoTracking()
|
||||
.Where(s => s.Dispatch != null && s.Dispatch.WorkOrderId == workOrderId && s.IsDeleted != true)
|
||||
.OrderByDescending(s => s.SignedAt)
|
||||
.Select(s => new DispatchSignoffRow(
|
||||
s.DispatchId,
|
||||
s.SignoffType,
|
||||
s.Name,
|
||||
s.SignedAt))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<WorkOrder?> GetWorkOrderForMediaAsync(int workOrderId)
|
||||
{
|
||||
return await WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking())
|
||||
.FirstOrDefaultAsync(w => w.Id == workOrderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public interface IWorkOrderDetailDataService
|
||||
{
|
||||
Task<bool> ExistsAsync(int workOrderId);
|
||||
Task<WorkOrderDetailExtendedFields?> GetExtendedFieldsAsync(int workOrderId);
|
||||
Task<IReadOnlyList<Comments>> GetCommentsAsync(int workOrderId);
|
||||
Task<IReadOnlyList<WorkOrderAuditLog>> GetAuditLogsAsync(int workOrderId, int? limit = null);
|
||||
Task<IReadOnlyList<WorkOrderAttachments>> GetAttachmentsAsync(int workOrderId);
|
||||
Task<IReadOnlyList<DispatchSignoffRow>> GetDispatchSignoffsAsync(int workOrderId);
|
||||
Task<WorkOrder?> GetWorkOrderForMediaAsync(int workOrderId);
|
||||
}
|
||||
|
||||
public interface ICompletionDocTemplateDataService
|
||||
{
|
||||
Task<IReadOnlyList<CompletionDocTemplateRow>> GetActiveAsync(string? serviceKey, WorkOrderType? workOrderType);
|
||||
Task<CompletionDocTemplateRow?> GetByIdAsync(int id);
|
||||
Task<CompletionDocTemplateRow?> ResolveForWorkOrderAsync(string? trade, WorkOrderType? workOrderType);
|
||||
Task<CompletionDocTemplate> CreateAsync(CompletionDocTemplate template);
|
||||
Task UpdateAsync(CompletionDocTemplate template);
|
||||
Task<bool> DeleteAsync(int id);
|
||||
}
|
||||
}
|
||||
30
SeaHaven.DataServices/Interfaces/WorkOrderDetailModels.cs
Normal file
30
SeaHaven.DataServices/Interfaces/WorkOrderDetailModels.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public record WorkOrderDetailExtendedFields(
|
||||
string? Description,
|
||||
string? Trade,
|
||||
string? Problem,
|
||||
string? SubTrade,
|
||||
DateOnly? OriginalWeek,
|
||||
DateOnly? OriginalDate,
|
||||
DateTime? CompletedDate,
|
||||
string? SignOffName,
|
||||
string? SignOffAttachment,
|
||||
string? SignOffSignature);
|
||||
|
||||
public record DispatchSignoffRow(
|
||||
int DispatchId,
|
||||
string? SignoffType,
|
||||
string? Name,
|
||||
DateTime? SignedAt);
|
||||
|
||||
public record CompletionDocTemplateRow(
|
||||
int Id,
|
||||
string Name,
|
||||
string ServiceKey,
|
||||
WorkOrderType? WorkOrderType,
|
||||
string TemplateUrl,
|
||||
bool IsActive);
|
||||
}
|
||||
104
SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs
Normal file
104
SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.DTOs
|
||||
{
|
||||
public class WorkOrderDetailDto
|
||||
{
|
||||
public WorkOrderDetailInfoDto Info { get; set; } = new();
|
||||
public WorkOrderCompletionDto Completion { get; set; } = new();
|
||||
public List<WorkOrderCommentDto> Comments { get; set; } = new();
|
||||
public List<WorkOrderAuditEntryDto> Audit { get; set; } = new();
|
||||
public List<WorkOrderMediaFileDto> Media { get; set; } = new();
|
||||
}
|
||||
|
||||
public class WorkOrderDetailInfoDto : WorkOrderBoardRowDto
|
||||
{
|
||||
public string? Description { get; set; }
|
||||
public string? Trade { get; set; }
|
||||
public string? Problem { get; set; }
|
||||
public string? SubTrade { get; set; }
|
||||
public DateOnly? OriginalWeek { get; set; }
|
||||
public DateOnly? OriginalDate { get; set; }
|
||||
public DateTime? CompletedDate { get; set; }
|
||||
}
|
||||
|
||||
public class CompletionDocTemplateDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string ServiceKey { get; set; } = "";
|
||||
public WorkOrderType? WorkOrderType { get; set; }
|
||||
public string TemplateUrl { get; set; } = "";
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class CompletionDocTemplateCreateDto
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string ServiceKey { get; set; } = "";
|
||||
public WorkOrderType? WorkOrderType { get; set; }
|
||||
public string TemplateUrl { get; set; } = "";
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
public class WorkOrderCompletionDto
|
||||
{
|
||||
public DocStatus? DocStatus { get; set; }
|
||||
public CompletionDocTemplateDto? Template { get; set; }
|
||||
public string? SignOffName { get; set; }
|
||||
public string? SignOffAttachment { get; set; }
|
||||
public string? SignOffSignature { get; set; }
|
||||
public List<DispatchSignoffDto> DispatchSignoffs { get; set; } = new();
|
||||
}
|
||||
|
||||
public class DispatchSignoffDto
|
||||
{
|
||||
public int DispatchId { get; set; }
|
||||
public string? SignoffType { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? SignedAt { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderCommentDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? AuthorId { get; set; }
|
||||
public string Text { get; set; } = "";
|
||||
public string Time { get; set; } = "";
|
||||
public string? Documents { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderCommentCreateDto
|
||||
{
|
||||
public string Text { get; set; } = "";
|
||||
}
|
||||
|
||||
public class WorkOrderAuditEntryDto
|
||||
{
|
||||
public string Type { get; set; } = "manual";
|
||||
public string? DispatcherId { get; set; }
|
||||
public string Action { get; set; } = "";
|
||||
public string? FieldName { get; set; }
|
||||
public string OldValue { get; set; } = "";
|
||||
public string NewValue { get; set; } = "";
|
||||
public string Time { get; set; } = "";
|
||||
public int? DispatchId { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderMediaFileDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public WorkOrderMediaCategory Category { get; set; }
|
||||
public string Url { get; set; } = "";
|
||||
public string? FileName { get; set; }
|
||||
public string? UploadedAt { get; set; }
|
||||
public bool IsLegacy { get; set; }
|
||||
}
|
||||
|
||||
public class WorkOrderCompletionDocUploadDto
|
||||
{
|
||||
public string? SignOffName { get; set; }
|
||||
public string? SignOffSignature { get; set; }
|
||||
public string? WorkOrderVersion { get; set; }
|
||||
}
|
||||
}
|
||||
41
SeaHaven.Services/Helpers/WorkOrderAuditProjection.cs
Normal file
41
SeaHaven.Services/Helpers/WorkOrderAuditProjection.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderAuditProjection
|
||||
{
|
||||
public static WorkOrderAuditEntryDto ToDto(WorkOrderAuditLog log)
|
||||
{
|
||||
return new WorkOrderAuditEntryDto
|
||||
{
|
||||
Type = MapEventType(log.EventType),
|
||||
DispatcherId = log.UserId,
|
||||
Action = log.Action ?? "",
|
||||
FieldName = log.FieldName,
|
||||
OldValue = log.OldValue ?? "",
|
||||
NewValue = log.NewValue ?? "",
|
||||
Time = log.CreatedAt.ToString("o"),
|
||||
DispatchId = log.DispatchId
|
||||
};
|
||||
}
|
||||
|
||||
public static List<WorkOrderAuditEntryDto> ToDtos(IEnumerable<WorkOrderAuditLog> logs)
|
||||
=> logs.Select(ToDto).ToList();
|
||||
|
||||
private static string MapEventType(string? eventType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(eventType))
|
||||
return "manual";
|
||||
|
||||
return eventType.ToLowerInvariant() switch
|
||||
{
|
||||
"manual" => "manual",
|
||||
"system" => "system",
|
||||
"sync" => "sync",
|
||||
"vendor" => "vendor",
|
||||
_ => eventType.ToLowerInvariant()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
23
SeaHaven.Services/Helpers/WorkOrderCommentProjection.cs
Normal file
23
SeaHaven.Services/Helpers/WorkOrderCommentProjection.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderCommentProjection
|
||||
{
|
||||
public static WorkOrderCommentDto ToDto(Comments comment)
|
||||
{
|
||||
return new WorkOrderCommentDto
|
||||
{
|
||||
Id = comment.Id,
|
||||
AuthorId = comment.UserId,
|
||||
Text = comment.Commenttext ?? "",
|
||||
Time = (comment.CreatedDate ?? DateTime.UtcNow).ToUniversalTime().ToString("o"),
|
||||
Documents = comment.Documents
|
||||
};
|
||||
}
|
||||
|
||||
public static List<WorkOrderCommentDto> ToDtos(IEnumerable<Comments> comments)
|
||||
=> comments.Select(ToDto).ToList();
|
||||
}
|
||||
}
|
||||
91
SeaHaven.Services/Helpers/WorkOrderMediaProjection.cs
Normal file
91
SeaHaven.Services/Helpers/WorkOrderMediaProjection.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderMediaProjection
|
||||
{
|
||||
public static List<WorkOrderMediaFileDto> ProjectAll(
|
||||
WorkOrder workOrder,
|
||||
IEnumerable<WorkOrderAttachments> attachments)
|
||||
{
|
||||
var media = new List<WorkOrderMediaFileDto>();
|
||||
var attachmentId = -1;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(workOrder.BeforPhotoAttachment))
|
||||
{
|
||||
media.Add(new WorkOrderMediaFileDto
|
||||
{
|
||||
Id = attachmentId--,
|
||||
Category = WorkOrderMediaCategory.Before,
|
||||
Url = workOrder.BeforPhotoAttachment,
|
||||
FileName = ExtractFileName(workOrder.BeforPhotoAttachment),
|
||||
IsLegacy = true
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(workOrder.AfterPhotoAttachment))
|
||||
{
|
||||
media.Add(new WorkOrderMediaFileDto
|
||||
{
|
||||
Id = attachmentId--,
|
||||
Category = WorkOrderMediaCategory.After,
|
||||
Url = workOrder.AfterPhotoAttachment,
|
||||
FileName = ExtractFileName(workOrder.AfterPhotoAttachment),
|
||||
IsLegacy = true
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(workOrder.SignOffAttachment))
|
||||
{
|
||||
media.Add(new WorkOrderMediaFileDto
|
||||
{
|
||||
Id = attachmentId--,
|
||||
Category = WorkOrderMediaCategory.Completion,
|
||||
Url = workOrder.SignOffAttachment,
|
||||
FileName = ExtractFileName(workOrder.SignOffAttachment),
|
||||
IsLegacy = true
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var attachment in attachments.Where(a => a.IsDeleted != true))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(attachment.Attachments))
|
||||
continue;
|
||||
|
||||
media.Add(new WorkOrderMediaFileDto
|
||||
{
|
||||
Id = attachment.Id,
|
||||
Category = attachment.Category ?? WorkOrderMediaCategory.Extra,
|
||||
Url = attachment.Attachments,
|
||||
FileName = ExtractFileName(attachment.Attachments),
|
||||
UploadedAt = attachment.CreatedDate?.ToUniversalTime().ToString("o"),
|
||||
IsLegacy = false
|
||||
});
|
||||
}
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
private static string? ExtractFileName(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return null;
|
||||
|
||||
var lastSlash = url.LastIndexOf('/');
|
||||
var segment = lastSlash >= 0 && lastSlash < url.Length - 1
|
||||
? url[(lastSlash + 1)..]
|
||||
: url;
|
||||
|
||||
try
|
||||
{
|
||||
return Uri.UnescapeDataString(segment);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return segment;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -120,10 +120,16 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
private void StageFieldLock(int workOrderId, string fieldName, string? userId)
|
||||
{
|
||||
var alreadyTracked = _context.WorkOrderFieldLocks.Local
|
||||
.Any(l => l.WorkOrderId == workOrderId && l.FieldName == fieldName);
|
||||
if (_context.WorkOrderFieldLocks.Local.Any(l =>
|
||||
l.WorkOrderId == workOrderId &&
|
||||
string.Equals(l.FieldName, fieldName, StringComparison.OrdinalIgnoreCase)))
|
||||
return;
|
||||
|
||||
if (alreadyTracked)
|
||||
var existsInDb = _context.WorkOrderFieldLocks.Any(l =>
|
||||
l.WorkOrderId == workOrderId &&
|
||||
l.FieldName == fieldName);
|
||||
|
||||
if (existsInDb)
|
||||
return;
|
||||
|
||||
_context.WorkOrderFieldLocks.Add(new WorkOrderFieldLock
|
||||
|
|
|
|||
59
SeaHaven.Services/Implementation/WorkOrderCommentService.cs
Normal file
59
SeaHaven.Services/Implementation/WorkOrderCommentService.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Exceptions;
|
||||
using SeaHaven.Services.Helpers;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
public class WorkOrderCommentService : IWorkOrderCommentService
|
||||
{
|
||||
private readonly IWorkOrderDetailDataService _detailData;
|
||||
private readonly ICommentDataService _commentData;
|
||||
|
||||
public WorkOrderCommentService(
|
||||
IWorkOrderDetailDataService detailData,
|
||||
ICommentDataService commentData)
|
||||
{
|
||||
_detailData = detailData;
|
||||
_commentData = commentData;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WorkOrderCommentDto>?> GetCommentsAsync(int workOrderId)
|
||||
{
|
||||
if (!await _detailData.ExistsAsync(workOrderId))
|
||||
return null;
|
||||
|
||||
var comments = await _detailData.GetCommentsAsync(workOrderId);
|
||||
return WorkOrderCommentProjection.ToDtos(comments);
|
||||
}
|
||||
|
||||
public async Task<WorkOrderCommentDto> AddCommentAsync(
|
||||
int workOrderId,
|
||||
WorkOrderCommentCreateDto request,
|
||||
string? actorId,
|
||||
string? documentUrl = null)
|
||||
{
|
||||
if (!await _detailData.ExistsAsync(workOrderId))
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Work order not found.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Text))
|
||||
throw new WorkOrderBoardValidationException("InvalidValue", "Comment text is required.");
|
||||
|
||||
var comment = new Comments
|
||||
{
|
||||
WorkerOrderId = workOrderId,
|
||||
UserId = actorId,
|
||||
Commenttext = request.Text.Trim(),
|
||||
CommentType = "General",
|
||||
RecordType = "WorkOrder",
|
||||
Documents = documentUrl,
|
||||
CreatedDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var saved = await _commentData.AddAsync(comment);
|
||||
return WorkOrderCommentProjection.ToDto(saved);
|
||||
}
|
||||
}
|
||||
}
|
||||
178
SeaHaven.Services/Implementation/WorkOrderCompletionService.cs
Normal file
178
SeaHaven.Services/Implementation/WorkOrderCompletionService.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Exceptions;
|
||||
using SeaHaven.Services.Helpers;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
public class WorkOrderCompletionService : IWorkOrderCompletionService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly ICompletionDocTemplateDataService _templateData;
|
||||
private readonly IWorkOrderDetailDataService _detailData;
|
||||
private readonly IWorkOrderAuditService _auditService;
|
||||
|
||||
public WorkOrderCompletionService(
|
||||
ApplicationDbContext context,
|
||||
ICompletionDocTemplateDataService templateData,
|
||||
IWorkOrderDetailDataService detailData,
|
||||
IWorkOrderAuditService auditService)
|
||||
{
|
||||
_context = context;
|
||||
_templateData = templateData;
|
||||
_detailData = detailData;
|
||||
_auditService = auditService;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CompletionDocTemplateDto>> GetTemplatesAsync(string? serviceKey, WorkOrderType? workOrderType)
|
||||
{
|
||||
var rows = await _templateData.GetActiveAsync(serviceKey, workOrderType);
|
||||
return rows.Select(WorkOrderDetailService.MapTemplate).ToList();
|
||||
}
|
||||
|
||||
public async Task<CompletionDocTemplateDto?> GetTemplateByIdAsync(int id)
|
||||
{
|
||||
var row = await _templateData.GetByIdAsync(id);
|
||||
return row == null ? null : WorkOrderDetailService.MapTemplate(row);
|
||||
}
|
||||
|
||||
public async Task<WorkOrderCompletionDto> UploadCompletionDocAsync(
|
||||
int workOrderId,
|
||||
WorkOrderCompletionDocUploadDto request,
|
||||
string fileUrl,
|
||||
string? actorId)
|
||||
{
|
||||
var workOrder = await _context.workOrders
|
||||
.FirstOrDefaultAsync(w => w.Id == workOrderId && w.istemplate != true && (w.IsDeleted != true || w.IsDeleted == null));
|
||||
|
||||
if (workOrder == null)
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Work order not found.");
|
||||
|
||||
if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus))
|
||||
throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status.");
|
||||
|
||||
var workOrderVersion = ParseRowVersion(request.WorkOrderVersion);
|
||||
if (workOrderVersion == null)
|
||||
throw new WorkOrderBoardValidationException("WorkOrderVersionRequired", "workOrderVersion is required.");
|
||||
|
||||
if (workOrder.RowVersion == null || !workOrder.RowVersion.AsSpan().SequenceEqual(workOrderVersion))
|
||||
throw new WorkOrderBoardValidationException("ConcurrencyConflict", "Work order was modified. Refresh and retry.");
|
||||
|
||||
_context.Entry(workOrder).Property(w => w.RowVersion).OriginalValue = workOrderVersion;
|
||||
|
||||
var oldDocStatus = workOrder.DocStatus?.ToString();
|
||||
var oldAttachment = workOrder.SignOffAttachment;
|
||||
|
||||
workOrder.SignOffAttachment = fileUrl;
|
||||
if (!string.IsNullOrWhiteSpace(request.SignOffName))
|
||||
workOrder.SignOffName = request.SignOffName.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(request.SignOffSignature))
|
||||
workOrder.SignOffSignature = request.SignOffSignature.Trim();
|
||||
|
||||
workOrder.DocStatus = DocStatus.Yes;
|
||||
|
||||
_auditService.StageFieldChanged(workOrderId, "SignOffAttachment", oldAttachment, fileUrl, actorId);
|
||||
if (oldDocStatus != DocStatus.Yes.ToString())
|
||||
_auditService.StageFieldChanged(workOrderId, "DocStatus", oldDocStatus, DocStatus.Yes.ToString(), actorId);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var extended = await _detailData.GetExtendedFieldsAsync(workOrderId);
|
||||
var template = await _templateData.ResolveForWorkOrderAsync(workOrder.Trade, workOrder.WorkOrderType);
|
||||
var signoffs = await _detailData.GetDispatchSignoffsAsync(workOrderId);
|
||||
|
||||
return new WorkOrderCompletionDto
|
||||
{
|
||||
DocStatus = workOrder.DocStatus,
|
||||
Template = template == null ? null : WorkOrderDetailService.MapTemplate(template),
|
||||
SignOffName = extended?.SignOffName,
|
||||
SignOffAttachment = extended?.SignOffAttachment,
|
||||
SignOffSignature = extended?.SignOffSignature,
|
||||
DispatchSignoffs = signoffs.Select(s => new DispatchSignoffDto
|
||||
{
|
||||
DispatchId = s.DispatchId,
|
||||
SignoffType = s.SignoffType,
|
||||
Name = s.Name,
|
||||
SignedAt = s.SignedAt?.ToUniversalTime().ToString("o")
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<CompletionDocTemplateDto> CreateTemplateAsync(CompletionDocTemplateCreateDto request)
|
||||
{
|
||||
ValidateTemplateRequest(request);
|
||||
|
||||
var entity = new CompletionDocTemplate
|
||||
{
|
||||
Name = request.Name.Trim(),
|
||||
ServiceKey = request.ServiceKey.Trim(),
|
||||
WorkOrderType = request.WorkOrderType,
|
||||
TemplateUrl = request.TemplateUrl.Trim(),
|
||||
IsActive = request.IsActive
|
||||
};
|
||||
|
||||
var created = await _templateData.CreateAsync(entity);
|
||||
return WorkOrderDetailService.MapTemplate(new CompletionDocTemplateRow(
|
||||
created.Id, created.Name, created.ServiceKey, created.WorkOrderType, created.TemplateUrl, created.IsActive));
|
||||
}
|
||||
|
||||
public async Task<CompletionDocTemplateDto> UpdateTemplateAsync(int id, CompletionDocTemplateCreateDto request)
|
||||
{
|
||||
ValidateTemplateRequest(request);
|
||||
|
||||
var entity = await _context.CompletionDocTemplates.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true);
|
||||
if (entity == null)
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Template not found.");
|
||||
|
||||
entity.Name = request.Name.Trim();
|
||||
entity.ServiceKey = request.ServiceKey.Trim();
|
||||
entity.WorkOrderType = request.WorkOrderType;
|
||||
entity.TemplateUrl = request.TemplateUrl.Trim();
|
||||
entity.IsActive = request.IsActive;
|
||||
|
||||
await _templateData.UpdateAsync(entity);
|
||||
return WorkOrderDetailService.MapTemplate(new CompletionDocTemplateRow(
|
||||
entity.Id, entity.Name, entity.ServiceKey, entity.WorkOrderType, entity.TemplateUrl, entity.IsActive));
|
||||
}
|
||||
|
||||
public async Task DeleteTemplateAsync(int id)
|
||||
{
|
||||
if (!await _templateData.DeleteAsync(id))
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Template not found.");
|
||||
}
|
||||
|
||||
private static void ValidateTemplateRequest(CompletionDocTemplateCreateDto request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.ServiceKey))
|
||||
throw new WorkOrderBoardValidationException("InvalidValue", "Name and serviceKey are required.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.TemplateUrl))
|
||||
throw new WorkOrderBoardValidationException("InvalidValue", "TemplateUrl is required.");
|
||||
|
||||
if (!Uri.TryCreate(request.TemplateUrl.Trim(), UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
|
||||
{
|
||||
throw new WorkOrderBoardValidationException("InvalidValue", "TemplateUrl must be an absolute http(s) URL.");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[]? ParseRowVersion(string? base64)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(base64))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return Convert.FromBase64String(base64);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
throw new WorkOrderBoardValidationException("InvalidRowVersion", "Invalid workOrderVersion format.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
149
SeaHaven.Services/Implementation/WorkOrderDetailService.cs
Normal file
149
SeaHaven.Services/Implementation/WorkOrderDetailService.cs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Helpers;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
public class WorkOrderDetailService : IWorkOrderDetailService
|
||||
{
|
||||
private readonly IWorkOrderBoardService _boardService;
|
||||
private readonly IWorkOrderDetailDataService _detailData;
|
||||
private readonly ICompletionDocTemplateDataService _templateData;
|
||||
|
||||
public WorkOrderDetailService(
|
||||
IWorkOrderBoardService boardService,
|
||||
IWorkOrderDetailDataService detailData,
|
||||
ICompletionDocTemplateDataService templateData)
|
||||
{
|
||||
_boardService = boardService;
|
||||
_detailData = detailData;
|
||||
_templateData = templateData;
|
||||
}
|
||||
|
||||
public async Task<WorkOrderDetailDto?> GetDetailAsync(int workOrderId)
|
||||
{
|
||||
if (!await _detailData.ExistsAsync(workOrderId))
|
||||
return null;
|
||||
|
||||
var boardRow = await _boardService.GetBoardRowAsync(workOrderId);
|
||||
if (boardRow == null)
|
||||
return null;
|
||||
|
||||
var extended = await _detailData.GetExtendedFieldsAsync(workOrderId);
|
||||
var comments = await _detailData.GetCommentsAsync(workOrderId);
|
||||
var audit = await _detailData.GetAuditLogsAsync(workOrderId, 50);
|
||||
var attachments = await _detailData.GetAttachmentsAsync(workOrderId);
|
||||
var signoffs = await _detailData.GetDispatchSignoffsAsync(workOrderId);
|
||||
var workOrder = await _detailData.GetWorkOrderForMediaAsync(workOrderId);
|
||||
|
||||
var template = await _templateData.ResolveForWorkOrderAsync(
|
||||
extended?.Trade ?? boardRow.Pm,
|
||||
boardRow.WorkOrderType);
|
||||
|
||||
return new WorkOrderDetailDto
|
||||
{
|
||||
Info = MapInfo(boardRow, extended),
|
||||
Completion = MapCompletion(extended, boardRow.DocStatus, template, signoffs),
|
||||
Comments = WorkOrderCommentProjection.ToDtos(comments),
|
||||
Audit = WorkOrderAuditProjection.ToDtos(audit),
|
||||
Media = workOrder == null
|
||||
? new List<WorkOrderMediaFileDto>()
|
||||
: WorkOrderMediaProjection.ProjectAll(workOrder, attachments)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WorkOrderAuditEntryDto>?> GetAuditAsync(int workOrderId, int limit = 50)
|
||||
{
|
||||
if (!await _detailData.ExistsAsync(workOrderId))
|
||||
return null;
|
||||
|
||||
var logs = await _detailData.GetAuditLogsAsync(workOrderId, limit);
|
||||
return WorkOrderAuditProjection.ToDtos(logs);
|
||||
}
|
||||
|
||||
private static WorkOrderDetailInfoDto MapInfo(
|
||||
WorkOrderBoardRowDto row,
|
||||
WorkOrderDetailExtendedFields? extended)
|
||||
{
|
||||
return new WorkOrderDetailInfoDto
|
||||
{
|
||||
Id = row.Id,
|
||||
WoNumber = row.WoNumber,
|
||||
RescheduleCount = row.RescheduleCount,
|
||||
CarriedOver = row.CarriedOver,
|
||||
WorkOrderType = row.WorkOrderType,
|
||||
IsPastDue = row.IsPastDue,
|
||||
SiteCode = row.SiteCode,
|
||||
LocationName = row.LocationName,
|
||||
PocName = row.PocName,
|
||||
PocPhone = row.PocPhone,
|
||||
PocNotes = row.PocNotes,
|
||||
LifecycleStatus = row.LifecycleStatus,
|
||||
LifecycleStatusLabel = row.LifecycleStatusLabel,
|
||||
LegacyStatus = row.LegacyStatus,
|
||||
DispatcherId = row.DispatcherId,
|
||||
DispatcherName = row.DispatcherName,
|
||||
Initials = row.Initials,
|
||||
Color = row.Color,
|
||||
DueDate = row.DueDate,
|
||||
ScheduledDate = row.ScheduledDate,
|
||||
TargetWeek = row.TargetWeek,
|
||||
ScheduleWeekOnly = row.ScheduleWeekOnly,
|
||||
DayGroup = row.DayGroup,
|
||||
VendorId = row.VendorId,
|
||||
VendorName = row.VendorName,
|
||||
TechName = row.TechName,
|
||||
TechPhone = row.TechPhone,
|
||||
ApptDate = row.ApptDate,
|
||||
ApptTime = row.ApptTime,
|
||||
Pm = row.Pm,
|
||||
DocStatus = row.DocStatus,
|
||||
PrimaryDispatchId = row.PrimaryDispatchId,
|
||||
RowVersion = row.RowVersion,
|
||||
DispatchRowVersion = row.DispatchRowVersion,
|
||||
Description = extended?.Description,
|
||||
Trade = extended?.Trade,
|
||||
Problem = extended?.Problem,
|
||||
SubTrade = extended?.SubTrade,
|
||||
OriginalWeek = extended?.OriginalWeek,
|
||||
OriginalDate = extended?.OriginalDate,
|
||||
CompletedDate = extended?.CompletedDate
|
||||
};
|
||||
}
|
||||
|
||||
private static WorkOrderCompletionDto MapCompletion(
|
||||
WorkOrderDetailExtendedFields? extended,
|
||||
Data.SeaHavenIndustries.Enums.DocStatus? docStatus,
|
||||
CompletionDocTemplateRow? template,
|
||||
IReadOnlyList<DispatchSignoffRow> signoffs)
|
||||
{
|
||||
return new WorkOrderCompletionDto
|
||||
{
|
||||
DocStatus = docStatus,
|
||||
Template = template == null ? null : MapTemplate(template),
|
||||
SignOffName = extended?.SignOffName,
|
||||
SignOffAttachment = extended?.SignOffAttachment,
|
||||
SignOffSignature = extended?.SignOffSignature,
|
||||
DispatchSignoffs = signoffs.Select(s => new DispatchSignoffDto
|
||||
{
|
||||
DispatchId = s.DispatchId,
|
||||
SignoffType = s.SignoffType,
|
||||
Name = s.Name,
|
||||
SignedAt = s.SignedAt?.ToUniversalTime().ToString("o")
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
internal static CompletionDocTemplateDto MapTemplate(CompletionDocTemplateRow template)
|
||||
=> new()
|
||||
{
|
||||
Id = template.Id,
|
||||
Name = template.Name,
|
||||
ServiceKey = template.ServiceKey,
|
||||
WorkOrderType = template.WorkOrderType,
|
||||
TemplateUrl = template.TemplateUrl,
|
||||
IsActive = template.IsActive
|
||||
};
|
||||
}
|
||||
}
|
||||
134
SeaHaven.Services/Implementation/WorkOrderMediaService.cs
Normal file
134
SeaHaven.Services/Implementation/WorkOrderMediaService.cs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Exceptions;
|
||||
using SeaHaven.Services.Helpers;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
public class WorkOrderMediaService : IWorkOrderMediaService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IWorkOrderDetailDataService _detailData;
|
||||
|
||||
public WorkOrderMediaService(ApplicationDbContext context, IWorkOrderDetailDataService detailData)
|
||||
{
|
||||
_context = context;
|
||||
_detailData = detailData;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WorkOrderMediaFileDto>?> GetMediaAsync(int workOrderId)
|
||||
{
|
||||
if (!await _detailData.ExistsAsync(workOrderId))
|
||||
return null;
|
||||
|
||||
var workOrder = await _detailData.GetWorkOrderForMediaAsync(workOrderId);
|
||||
if (workOrder == null)
|
||||
return null;
|
||||
|
||||
var attachments = await _detailData.GetAttachmentsAsync(workOrderId);
|
||||
return WorkOrderMediaProjection.ProjectAll(workOrder, attachments);
|
||||
}
|
||||
|
||||
public async Task<WorkOrderMediaFileDto> AddMediaAsync(
|
||||
int workOrderId,
|
||||
WorkOrderMediaCategory category,
|
||||
string fileUrl,
|
||||
string? actorId)
|
||||
{
|
||||
if (!await _detailData.ExistsAsync(workOrderId))
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Work order not found.");
|
||||
|
||||
var workOrder = await _context.workOrders.FirstOrDefaultAsync(w => w.Id == workOrderId);
|
||||
if (workOrder == null)
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Work order not found.");
|
||||
|
||||
if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus))
|
||||
throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status.");
|
||||
|
||||
if (category == WorkOrderMediaCategory.Completion)
|
||||
{
|
||||
throw new WorkOrderBoardValidationException(
|
||||
"UseCompletionDocEndpoint",
|
||||
"Completion documents must be uploaded via POST /api/workorders/{id}/completion-doc.");
|
||||
}
|
||||
|
||||
if (category == WorkOrderMediaCategory.Before)
|
||||
{
|
||||
workOrder.BeforPhotoAttachment = fileUrl;
|
||||
await _context.SaveChangesAsync();
|
||||
return new WorkOrderMediaFileDto
|
||||
{
|
||||
Id = -1,
|
||||
Category = category,
|
||||
Url = fileUrl,
|
||||
IsLegacy = true
|
||||
};
|
||||
}
|
||||
|
||||
if (category == WorkOrderMediaCategory.After)
|
||||
{
|
||||
workOrder.AfterPhotoAttachment = fileUrl;
|
||||
await _context.SaveChangesAsync();
|
||||
return new WorkOrderMediaFileDto
|
||||
{
|
||||
Id = -2,
|
||||
Category = category,
|
||||
Url = fileUrl,
|
||||
IsLegacy = true
|
||||
};
|
||||
}
|
||||
|
||||
var attachment = new WorkOrderAttachments
|
||||
{
|
||||
WorkorderId = workOrderId,
|
||||
Attachments = fileUrl,
|
||||
Category = category,
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
createdby = actorId
|
||||
};
|
||||
|
||||
_context.workOrderAttachments.Add(attachment);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return new WorkOrderMediaFileDto
|
||||
{
|
||||
Id = attachment.Id,
|
||||
Category = category,
|
||||
Url = fileUrl,
|
||||
UploadedAt = attachment.CreatedDate?.ToUniversalTime().ToString("o"),
|
||||
IsLegacy = false
|
||||
};
|
||||
}
|
||||
|
||||
public async Task DeleteMediaAsync(int workOrderId, int mediaId, string? actorId)
|
||||
{
|
||||
if (mediaId <= 0)
|
||||
throw new WorkOrderBoardValidationException("InvalidMedia", "Legacy media cannot be deleted via this endpoint.");
|
||||
|
||||
if (!await _detailData.ExistsAsync(workOrderId))
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Work order not found.");
|
||||
|
||||
var workOrder = await _context.workOrders.FirstOrDefaultAsync(w => w.Id == workOrderId);
|
||||
if (workOrder == null)
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Work order not found.");
|
||||
|
||||
if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus))
|
||||
throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status.");
|
||||
|
||||
var attachment = await _context.workOrderAttachments
|
||||
.FirstOrDefaultAsync(a => a.Id == mediaId && a.WorkorderId == workOrderId && a.IsDeleted != true);
|
||||
|
||||
if (attachment == null)
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Media not found.");
|
||||
|
||||
attachment.IsDeleted = true;
|
||||
attachment.DeletionTime = DateTime.UtcNow;
|
||||
attachment.DeleterUserId = actorId;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
SeaHaven.Services/Interfaces/IWorkOrderCommentService.cs
Normal file
11
SeaHaven.Services/Interfaces/IWorkOrderCommentService.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Interfaces
|
||||
{
|
||||
public interface IWorkOrderCommentService
|
||||
{
|
||||
Task<IReadOnlyList<WorkOrderCommentDto>?> GetCommentsAsync(int workOrderId);
|
||||
Task<WorkOrderCommentDto> AddCommentAsync(int workOrderId, WorkOrderCommentCreateDto request, string? actorId, string? documentUrl = null);
|
||||
}
|
||||
}
|
||||
19
SeaHaven.Services/Interfaces/IWorkOrderCompletionService.cs
Normal file
19
SeaHaven.Services/Interfaces/IWorkOrderCompletionService.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Interfaces
|
||||
{
|
||||
public interface IWorkOrderCompletionService
|
||||
{
|
||||
Task<IReadOnlyList<CompletionDocTemplateDto>> GetTemplatesAsync(string? serviceKey, WorkOrderType? workOrderType);
|
||||
Task<CompletionDocTemplateDto?> GetTemplateByIdAsync(int id);
|
||||
Task<WorkOrderCompletionDto> UploadCompletionDocAsync(
|
||||
int workOrderId,
|
||||
WorkOrderCompletionDocUploadDto request,
|
||||
string fileUrl,
|
||||
string? actorId);
|
||||
Task<CompletionDocTemplateDto> CreateTemplateAsync(CompletionDocTemplateCreateDto request);
|
||||
Task<CompletionDocTemplateDto> UpdateTemplateAsync(int id, CompletionDocTemplateCreateDto request);
|
||||
Task DeleteTemplateAsync(int id);
|
||||
}
|
||||
}
|
||||
10
SeaHaven.Services/Interfaces/IWorkOrderDetailService.cs
Normal file
10
SeaHaven.Services/Interfaces/IWorkOrderDetailService.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Interfaces
|
||||
{
|
||||
public interface IWorkOrderDetailService
|
||||
{
|
||||
Task<WorkOrderDetailDto?> GetDetailAsync(int workOrderId);
|
||||
Task<IReadOnlyList<WorkOrderAuditEntryDto>?> GetAuditAsync(int workOrderId, int limit = 50);
|
||||
}
|
||||
}
|
||||
12
SeaHaven.Services/Interfaces/IWorkOrderMediaService.cs
Normal file
12
SeaHaven.Services/Interfaces/IWorkOrderMediaService.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Interfaces
|
||||
{
|
||||
public interface IWorkOrderMediaService
|
||||
{
|
||||
Task<IReadOnlyList<WorkOrderMediaFileDto>?> GetMediaAsync(int workOrderId);
|
||||
Task<WorkOrderMediaFileDto> AddMediaAsync(int workOrderId, WorkOrderMediaCategory category, string fileUrl, string? actorId);
|
||||
Task DeleteMediaAsync(int workOrderId, int mediaId, string? actorId);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue