mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 09:33:13 +00:00
A standalone severity patch on an Overdue WO still stores the value, because the board patches severity before type when correcting Overdue to Emergency/Reactive and that write must not be dropped or rejected. Project the severity as null for Overdue in the board row mapping, which also feeds the PATCH response, search results and the detail view, so a stored value never surfaces on a type that carries no severity.
251 lines
11 KiB
C#
251 lines
11 KiB
C#
using System.Security.Claims;
|
|
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using SeaHaven.DataServices.Helpers;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Helpers;
|
|
using SeaHaven.Services.Interfaces;
|
|
|
|
namespace SeaHaven.Services.Implementation
|
|
{
|
|
public class WorkOrderBoardService : IWorkOrderBoardService
|
|
{
|
|
private readonly IWorkOrderBoardDataService _boardDataService;
|
|
private readonly IWorkOrderAccountResolver _accountResolver;
|
|
|
|
public WorkOrderBoardService(
|
|
IWorkOrderBoardDataService boardDataService,
|
|
IWorkOrderAccountResolver accountResolver)
|
|
{
|
|
_boardDataService = boardDataService;
|
|
_accountResolver = accountResolver;
|
|
}
|
|
|
|
public async Task<WorkOrderBoardResponseDto> GetBoardAsync(
|
|
WorkOrderBoardQueryDto query,
|
|
ClaimsPrincipal user,
|
|
string? currentUserId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var accountId = _accountResolver.ResolveAccountFilter(user);
|
|
var weekStart = query.WeekStart;
|
|
var weekEnd = query.WeekEnd ?? weekStart.AddDays(4);
|
|
|
|
var validationError = WorkOrderOperationalWeek.ValidateWeekWindow(weekStart, weekEnd);
|
|
if (validationError != null)
|
|
throw new ArgumentException(validationError);
|
|
|
|
var dataQuery = new WorkOrderBoardQuery(
|
|
weekStart,
|
|
weekEnd,
|
|
query.Dispatchers,
|
|
query.MyWorkOrders,
|
|
query.Types,
|
|
query.Overdue,
|
|
query.Search,
|
|
currentUserId,
|
|
accountId);
|
|
|
|
var result = await _boardDataService.GetBoardRowsAsync(dataQuery, cancellationToken);
|
|
var utcNow = DateTime.UtcNow;
|
|
|
|
return new WorkOrderBoardResponseDto
|
|
{
|
|
WeekStart = weekStart,
|
|
WeekEnd = weekEnd,
|
|
Counts = new WorkOrderBoardCountsDto
|
|
{
|
|
Total = result.ScheduledTotalBeforeSearch,
|
|
Returned = result.ScheduledRows.Count
|
|
},
|
|
Scheduled = result.ScheduledRows.Select(r => MapRawRow(r, utcNow)).ToList(),
|
|
Unscheduled = result.UnscheduledRows.Select(r => MapRawRow(r, utcNow)).ToList()
|
|
};
|
|
}
|
|
|
|
public async Task<IReadOnlyList<DispatcherLookupDto>> GetDispatcherLookupsAsync()
|
|
{
|
|
var rows = await _boardDataService.GetDispatcherLookupsAsync();
|
|
return rows.Select(r => new DispatcherLookupDto
|
|
{
|
|
Id = r.Id,
|
|
Name = FormatName(r.FirstName, r.LastName),
|
|
Initials = r.Initials,
|
|
Color = r.Color
|
|
}).ToList();
|
|
}
|
|
|
|
public async Task<WorkOrderBoardRowDto?> GetBoardRowAsync(int workOrderId, ClaimsPrincipal user)
|
|
{
|
|
var accountId = _accountResolver.ResolveAccountFilter(user);
|
|
var raw = await _boardDataService.GetBoardRowByIdAsync(workOrderId, accountId);
|
|
return raw == null ? null : MapRawRow(raw, DateTime.UtcNow);
|
|
}
|
|
|
|
public static WorkOrderBoardRowDto MapRawRow(WorkOrderBoardRawRow row, DateTime utcNow)
|
|
{
|
|
// Appt column: date prefers dispatch appointment (vendor slot), then WO ScheduledDate.
|
|
// Time uses WO ScheduledStart/End only. Do not fall back to a date-only dispatch
|
|
// instant (midnight), or clearing apptTime still renders 00:00.
|
|
var apptStart = row.ScheduledStart
|
|
?? (HasClockTime(row.DispatchApptDate) ? row.DispatchApptDate : null);
|
|
var apptEnd = row.ScheduledEnd;
|
|
var (vendorId, vendorName) = WorkOrderBoardDispatchAssignment.LiveVendor(
|
|
row.VendorId,
|
|
row.VendorName,
|
|
row.PrimaryDispatchStatus);
|
|
|
|
return new WorkOrderBoardRowDto
|
|
{
|
|
Id = row.Id,
|
|
WoNumber = row.InternalWONumber,
|
|
RescheduleCount = row.RescheduleCount,
|
|
CarriedOver = row.CarriedOver,
|
|
IsAddOn = row.IsAddOn,
|
|
AvetaRequired = row.AvetaRequired,
|
|
HasAvetaDocument = row.HasAvetaDocument,
|
|
OriginalDate = row.OriginalDate,
|
|
OriginalWeek = row.OriginalWeek,
|
|
WorkOrderType = row.WorkOrderType,
|
|
ServiceId = row.ServiceId,
|
|
ServiceNameSnapshot = row.ServiceNameSnapshot,
|
|
IsPastDue = WorkOrderDerivedFields.IsPastDue(row.ScheduledDate, row.LifecycleStatus, utcNow),
|
|
SiteCode = row.SiteCode,
|
|
LocationName = row.LocationName,
|
|
PocName = row.PocName,
|
|
PocPhone = row.PocPhone,
|
|
PocNotes = row.PocNotes,
|
|
PocCustomized = !string.IsNullOrWhiteSpace(row.WoPocName) || !string.IsNullOrWhiteSpace(row.WoPocPhone),
|
|
FrozenSite = MapFrozenSite(row.FrozenSite),
|
|
FrozenCompany = MapFrozenCompany(row.FrozenCompany),
|
|
FrozenPoc = MapFrozenPoc(row.FrozenPoc),
|
|
LifecycleStatus = row.LifecycleStatus,
|
|
LifecycleStatusLabel = WorkOrderDerivedFields.GetLifecycleStatusLabel(row.LifecycleStatus),
|
|
LegacyStatus = row.LegacyStatus,
|
|
DispatcherId = row.AssignTo,
|
|
DispatcherName = FormatName(row.DispatcherFirstName, row.DispatcherLastName),
|
|
Initials = row.Initials,
|
|
Color = row.Color,
|
|
DueDate = row.DueDate,
|
|
ScheduledDate = row.ScheduledDate,
|
|
TargetWeek = row.TargetWeek,
|
|
ScheduleWeekOnly = row.ScheduleWeekOnly,
|
|
DayGroup = WorkOrderDerivedFields.GetDayGroup(row.ScheduledDate),
|
|
VendorId = vendorId,
|
|
VendorName = vendorName,
|
|
PrimaryDispatchStatus = row.PrimaryDispatchStatus,
|
|
TechName = row.TechName,
|
|
TechPhone = row.TechPhone,
|
|
ApptDate = row.DispatchApptDate ?? row.ScheduledDate,
|
|
ApptTime = WorkOrderDerivedFields.FormatApptTime(apptStart, apptEnd),
|
|
Pm = row.Trade ?? row.ServiceNameSnapshot ?? row.Problem,
|
|
ServiceNotes = row.ServiceNotes,
|
|
ExtraServices = ParseExtraServices(row.ExtraServices),
|
|
AdditionalContacts = MapAdditionalContacts(row),
|
|
DocStatus = row.DocStatus,
|
|
MediaCount = row.MediaCount,
|
|
CompletedDate = row.CompletedDate,
|
|
FlagColor = row.FlagColor,
|
|
// Overdue never carries a severity. A standalone severity patch can still
|
|
// store one (the board patches severity before type when correcting Overdue
|
|
// to Emergency/Reactive), so the row never returns it for an Overdue WO.
|
|
Severity = row.WorkOrderType == WorkOrderType.Overdue
|
|
? null
|
|
: WorkOrderSeverityRules.ParseLevel(row.Severity),
|
|
PrimaryDispatchId = row.PrimaryDispatchId,
|
|
RowVersion = row.RowVersion,
|
|
DispatchRowVersion = row.DispatchRowVersion,
|
|
PendingUpliftCount = row.PendingUpliftCount,
|
|
UpliftSummary = WorkOrderUpliftContractMapper.MapSummary(row)
|
|
};
|
|
}
|
|
|
|
private static List<WorkOrderAdditionalContactDto>? MapAdditionalContacts(
|
|
WorkOrderBoardRawRow row)
|
|
{
|
|
if (row.FrozenPoc == null)
|
|
return WorkOrderAdditionalContactsMapper.ParseJson(row.AdditionalContacts);
|
|
|
|
var additional = row.FrozenPoc.Contacts
|
|
.Skip(1)
|
|
.Where(contact => !string.IsNullOrWhiteSpace(contact.Name)
|
|
|| !string.IsNullOrWhiteSpace(contact.Phone))
|
|
.Select(contact => new WorkOrderAdditionalContactDto(
|
|
contact.Name ?? "",
|
|
contact.Phone ?? "",
|
|
null))
|
|
.ToList();
|
|
return additional.Count > 0 ? additional : null;
|
|
}
|
|
|
|
private static WorkOrderFrozenSiteDto? MapFrozenSite(WorkOrderFrozenSite? site)
|
|
=> site == null
|
|
? null
|
|
: new WorkOrderFrozenSiteDto
|
|
{
|
|
Code = site.Code,
|
|
Label = site.Label,
|
|
Address = site.Address,
|
|
City = site.City,
|
|
State = site.State,
|
|
Zip = site.Zip,
|
|
Phone = site.Phone,
|
|
Email = site.Email,
|
|
Maps = site.Maps
|
|
};
|
|
|
|
private static WorkOrderFrozenCompanyDto? MapFrozenCompany(WorkOrderFrozenCompany? company)
|
|
=> company == null
|
|
? null
|
|
: new WorkOrderFrozenCompanyDto
|
|
{
|
|
Name = company.Name,
|
|
ContactName = company.ContactName,
|
|
Address = company.Address,
|
|
City = company.City,
|
|
State = company.State,
|
|
Zip = company.Zip,
|
|
Phone = company.Phone,
|
|
Email = company.Email,
|
|
Maps = company.Maps
|
|
};
|
|
|
|
private static WorkOrderFrozenPocDto? MapFrozenPoc(WorkOrderFrozenPoc? poc)
|
|
=> poc == null
|
|
? null
|
|
: new WorkOrderFrozenPocDto
|
|
{
|
|
Notes = poc.Notes,
|
|
Contacts = poc.Contacts
|
|
.Select(contact => new WorkOrderFrozenPocContactDto
|
|
{
|
|
Name = contact.Name,
|
|
Phone = contact.Phone
|
|
})
|
|
.ToList()
|
|
};
|
|
|
|
private static bool HasClockTime(DateTime? value)
|
|
=> value.HasValue && value.Value.TimeOfDay != TimeSpan.Zero;
|
|
|
|
private static string FormatName(string? firstName, string? lastName)
|
|
=> $"{firstName ?? ""} {lastName ?? ""}".Trim();
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|