using System.Security.Claims; 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 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> 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 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, IsPastDue = WorkOrderDerivedFields.IsPastDue(row.ScheduledDate, row.LifecycleStatus, utcNow), SiteCode = row.SiteCode, LocationName = row.LocationName, PocName = row.PocName, PocPhone = row.PocPhone, PocNotes = row.PocNotes, 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.Problem, ServiceNotes = row.ServiceNotes, ExtraServices = ParseExtraServices(row.ExtraServices), AdditionalContacts = WorkOrderAdditionalContactsMapper.ParseJson(row.AdditionalContacts), DocStatus = row.DocStatus, MediaCount = row.MediaCount, CompletedDate = row.CompletedDate, FlagColor = row.FlagColor, Severity = WorkOrderSeverityRules.ParseLevel(row.Severity), PrimaryDispatchId = row.PrimaryDispatchId, RowVersion = row.RowVersion, DispatchRowVersion = row.DispatchRowVersion, PendingUpliftCount = row.PendingUpliftCount, UpliftSummary = WorkOrderUpliftContractMapper.MapSummary(row) }; } 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? ParseExtraServices(string? json) { if (string.IsNullOrWhiteSpace(json)) return null; try { var list = System.Text.Json.JsonSerializer.Deserialize>(json); return list is { Count: > 0 } ? list : null; } catch (System.Text.Json.JsonException) { return null; } } } }