mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 10:43:13 +00:00
Stamp WorkOrder.AccountId on all create paths and filter board/list/search/detail by server-derived account claims so scoped callers cannot cross accounts. Co-authored-by: Cursor <cursoragent@cursor.com>
192 lines
8.1 KiB
C#
192 lines
8.1 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,
|
|
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,
|
|
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
|
|
};
|
|
}
|
|
}
|