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

254 lines
11 KiB
C#
Raw Normal View History

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,
TechnicianAssigned = row.TechnicianAssigned,
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,
TechnicianAssigned = company.TechnicianAssigned,
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;
}
}
}
}