shoc-backend/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs

210 lines
10 KiB
C#

using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Exceptions;
using SeaHaven.Services.Helpers;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
namespace SeaHaven.Services.Implementation
{
public class WorkOrderBoardCreateService : IWorkOrderBoardCreateService
{
private readonly ApplicationDbContext _context;
private readonly IWorkOrderBoardDataService _boardDataService;
private readonly IWorkOrderBoardService _boardService;
private readonly IWorkOrderAuditService _auditService;
private readonly IWorkOrderBoardCreateValidation _validator;
public WorkOrderBoardCreateService(
ApplicationDbContext context,
IWorkOrderBoardDataService boardDataService,
IWorkOrderBoardService boardService,
IWorkOrderAuditService auditService,
IWorkOrderBoardCreateValidation validator)
{
_context = context;
_boardDataService = boardDataService;
_boardService = boardService;
_auditService = auditService;
_validator = validator;
}
public async Task<WorkOrderBoardRowDto> CreateAsync(WorkOrderBoardCreateRequestDto request, string? actorId)
{
var validationResult = await _validator.ValidateAsync(request);
if (!validationResult.IsValid)
throw new ValidationException(validationResult.Errors);
var woNumber = await ResolveWoNumberAsync(request.WoNumber);
var siteCode = request.SiteCode!.Trim();
var workOrder = new WorkOrder
{
InternalWONumber = woNumber,
WorkerOrderNumber = woNumber,
WorkOrderType = request.WorkOrderType,
SiteCode = siteCode,
Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(),
Trade = string.IsNullOrWhiteSpace(request.Trade) ? null : request.Trade.Trim(),
LocationId = request.LocationId,
DueDate = request.DueDate?.Date,
AssignTo = string.IsNullOrWhiteSpace(request.AssignTo) ? null : request.AssignTo.Trim(),
TargetWeek = request.TargetWeek,
ScheduleWeekOnly = request.ScheduleWeekOnly,
DocStatus = request.DocStatus,
createdby = actorId,
CreatedDate = DateTime.UtcNow,
istemplate = false
};
WorkOrderBoardFieldMutations.SetInitialLifecycleStatus(workOrder);
var changes = new List<WorkOrderBoardFieldMutations.BoardFieldChange>
{
WorkOrderBoardFieldMutations.BoardFieldChange.Changed("InternalWONumber", "", woNumber),
WorkOrderBoardFieldMutations.BoardFieldChange.Changed("WorkOrderType", "", request.WorkOrderType.ToString()),
WorkOrderBoardFieldMutations.BoardFieldChange.Changed("SiteCode", "", siteCode),
WorkOrderBoardFieldMutations.BoardFieldChange.Changed("LifecycleStatus", "", LifecycleStatus.Incomplete.ToString())
};
if (workOrder.Description != null)
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("Description", "", workOrder.Description));
if (workOrder.Trade != null)
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("Trade", "", workOrder.Trade));
if (workOrder.LocationId.HasValue)
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("LocationId", "", workOrder.LocationId.Value.ToString()));
if (workOrder.DueDate.HasValue)
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("DueDate", "", workOrder.DueDate.Value.ToString("yyyy-MM-dd")));
if (workOrder.TargetWeek.HasValue)
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("TargetWeek", "", workOrder.TargetWeek.Value.ToString("yyyy-MM-dd")));
if (workOrder.ScheduleWeekOnly.HasValue)
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ScheduleWeekOnly", "", workOrder.ScheduleWeekOnly.Value.ToString()));
if (workOrder.DocStatus.HasValue)
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("DocStatus", "", workOrder.DocStatus.Value.ToString()));
if (!string.IsNullOrWhiteSpace(request.AssignTo))
changes.AddRange(WorkOrderBoardFieldMutations.ApplyAssignTo(workOrder, request.AssignTo));
else if (!string.IsNullOrWhiteSpace(workOrder.AssignTo))
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.AssignmentChanged("AssignTo", "", workOrder.AssignTo));
if (request.ScheduledDate.HasValue)
changes.AddRange(WorkOrderBoardFieldMutations.ApplyScheduledDate(workOrder, request.ScheduledDate.Value.Date));
Dispatch? dispatch = null;
if (request.VendorId.HasValue || request.ApptDate.HasValue || !string.IsNullOrWhiteSpace(request.ApptTime))
{
if (!request.VendorId.HasValue || request.VendorId.Value <= 0)
throw new WorkOrderBoardValidationException("DispatchRequired", "vendorId is required when setting appointment fields.");
dispatch = new Dispatch
{
VendorId = request.VendorId.Value,
Status = "Pending",
CreatedDate = DateTime.UtcNow,
ScheduledDate = request.ApptDate?.Date
};
_context.Dispatches.Add(dispatch);
workOrder.PrimaryDispatch = dispatch;
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("VendorId", "", request.VendorId.Value.ToString()));
if (request.ApptDate.HasValue)
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ApptDate", "", request.ApptDate.Value.ToString("yyyy-MM-dd")));
if (!string.IsNullOrWhiteSpace(request.ApptTime))
{
var apptChange = WorkOrderBoardFieldMutations.ApplyApptTime(workOrder, dispatch, request.ApptTime);
if (apptChange.HasChanged)
changes.Add(apptChange);
}
}
_context.workOrders.Add(workOrder);
_auditService.StageCreated(workOrder.Id, actorId, woNumber);
StageChanges(workOrder.Id, changes, actorId, dispatch);
await _context.SaveChangesAsync();
if (dispatch != null && workOrder.PrimaryDispatchId == null)
{
workOrder.PrimaryDispatchId = dispatch.Id;
dispatch.WorkOrderId = workOrder.Id;
await _context.SaveChangesAsync();
}
if (request.PocContactId.HasValue)
{
_context.WorkOrderContacts.Add(new WorkOrderContacts
{
WorkorderId = workOrder.Id,
ContactId = request.PocContactId,
Notes = string.IsNullOrWhiteSpace(request.PocNotes) ? null : request.PocNotes.Trim()
});
await _context.SaveChangesAsync();
}
var row = await _boardService.GetBoardRowAsync(workOrder.Id);
return row ?? throw new WorkOrderBoardValidationException("NotFound", "Work order was created but could not be loaded.");
}
private async Task<string> ResolveWoNumberAsync(string? requested)
{
if (!string.IsNullOrWhiteSpace(requested))
{
if (!WorkOrderNumberNormalizer.TryNormalize(requested, out var normalized, out var error))
throw new WorkOrderBoardValidationException("InvalidWoNumber", error ?? "Invalid WO number.");
if (await _boardDataService.InternalWoNumberExistsAsync(normalized, 0))
throw new WorkOrderBoardValidationException("DuplicateWoNumber", "WO number already exists.");
return normalized;
}
for (var attempt = 0; attempt < 20; attempt++)
{
var candidate = await GenerateNextSequentialWoNumberAsync();
if (!await _boardDataService.InternalWoNumberExistsAsync(candidate, 0))
return candidate;
}
throw new WorkOrderBoardValidationException("WoNumberGenerationFailed", "Could not generate a unique WO number.");
}
private async Task<string> GenerateNextSequentialWoNumberAsync()
{
var maxId = await _context.workOrders.MaxAsync(w => (int?)w.Id) ?? 0;
var seed = Math.Max(maxId + 1, 1);
if (!WorkOrderNumberNormalizer.TryNormalize(seed.ToString(), out var normalized, out _))
normalized = seed.ToString().PadLeft(11, '0');
return normalized;
}
private void StageChanges(
int workOrderId,
IEnumerable<WorkOrderBoardFieldMutations.BoardFieldChange> changes,
string? actorId,
Dispatch? dispatch)
{
foreach (var change in changes.Where(c => c.HasChanged))
{
var dispatchId = change.DispatchId ?? (dispatch?.Id > 0 ? dispatch.Id : null);
switch (change.Action)
{
case AuditActionType.StatusChanged:
_auditService.StageStatusChanged(workOrderId, change.OldValue, change.NewValue, actorId);
break;
case AuditActionType.AssignmentChanged:
_auditService.StageAssignmentChanged(workOrderId, change.OldValue, change.NewValue, actorId);
break;
default:
_auditService.StageFieldChanged(workOrderId, change.FieldName, change.OldValue ?? "", change.NewValue, actorId, dispatchId);
break;
}
}
}
}
}