shoc-backend/SeaHaven.Services/Implementation/WorkOrderDetailService.cs
Arthur Bassi 3609365939 fix(work-orders): map additionalContacts on detail GET (SH-218)
Copy contacts into MapInfo so slide-over round-trips create/PATCH, and require name plus phone on retained entries while dropping blank placeholders.
2026-08-18 10:03:12 -03:00

194 lines
8.2 KiB
C#

using System.Security.Claims;
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;
private readonly IUserDataService _userDataService;
private readonly IWorkOrderAccountResolver _accountResolver;
public WorkOrderDetailService(
IWorkOrderBoardService boardService,
IWorkOrderDetailDataService detailData,
ICompletionDocTemplateDataService templateData,
IUserDataService userDataService,
IWorkOrderAccountResolver accountResolver)
{
_boardService = boardService;
_detailData = detailData;
_templateData = templateData;
_userDataService = userDataService;
_accountResolver = accountResolver;
}
public async Task<WorkOrderDetailDto?> GetDetailAsync(int workOrderId, ClaimsPrincipal user)
{
var accountId = _accountResolver.ResolveAccountFilter(user);
if (!await _detailData.ExistsAsync(workOrderId, CancellationToken.None, accountId))
return null;
var boardRow = await _boardService.GetBoardRowAsync(workOrderId, user);
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, CancellationToken.None, accountId);
var template = await _templateData.ResolveForWorkOrderAsync(
extended?.Trade ?? boardRow.Pm,
boardRow.WorkOrderType);
var authorIds = WorkOrderCommentProjection.CollectAuthorUserIds(comments);
var authorNames = await _userDataService.GetDisplayNamesByIdsAsync(authorIds);
return new WorkOrderDetailDto
{
Info = MapInfo(boardRow, extended),
Completion = MapCompletion(extended, boardRow.DocStatus, template, signoffs),
Comments = WorkOrderCommentProjection.ToDtos(comments, authorNames),
Audit = await MapAuditAsync(audit),
Media = workOrder == null
? new List<WorkOrderMediaFileDto>()
: WorkOrderMediaProjection.ProjectAll(workOrder, attachments)
};
}
public async Task<IReadOnlyList<WorkOrderAuditEntryDto>?> GetAuditAsync(
int workOrderId,
ClaimsPrincipal user,
int limit = 50)
{
var accountId = _accountResolver.ResolveAccountFilter(user);
if (!await _detailData.ExistsAsync(workOrderId, CancellationToken.None, accountId))
return null;
var logs = await _detailData.GetAuditLogsAsync(workOrderId, limit);
return await MapAuditAsync(logs);
}
private async Task<List<WorkOrderAuditEntryDto>> MapAuditAsync(IReadOnlyList<Data.SeaHavenIndustries.WorkOrderAuditLog> logs)
{
var userIds = WorkOrderAuditProjection.CollectAssignToUserIds(logs);
var displayNames = await _userDataService.GetDisplayNamesByIdsAsync(userIds);
return WorkOrderAuditProjection.ToDtos(logs, displayNames);
}
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,
AdditionalContacts = row.AdditionalContacts,
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,
ServiceNotes = extended?.ServiceNotes ?? row.ServiceNotes,
ExtraServices = ParseExtraServices(extended?.ExtraServices) ?? row.ExtraServices,
DocStatus = row.DocStatus,
FlagColor = row.FlagColor,
PrimaryDispatchId = row.PrimaryDispatchId,
RowVersion = row.RowVersion,
DispatchRowVersion = row.DispatchRowVersion,
PendingUpliftCount = row.PendingUpliftCount,
Description = extended?.Description,
Trade = extended?.Trade,
Problem = extended?.Problem,
SubTrade = extended?.SubTrade,
OriginalWeek = extended?.OriginalWeek,
OriginalDate = extended?.OriginalDate,
CompletedDate = extended?.CompletedDate,
VendorNotes = extended?.VendorNotes
};
}
private static List<string>? ParseExtraServices(string? json)
{
if (string.IsNullOrWhiteSpace(json))
return null;
try
{
var list = System.Text.Json.JsonSerializer.Deserialize<List<string>>(json);
return list is { Count: > 0 } ? list : null;
}
catch (System.Text.Json.JsonException)
{
return null;
}
}
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
};
}
}