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 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, 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.Problem, ServiceNotes = row.ServiceNotes, ExtraServices = ParseExtraServices(row.ExtraServices), AdditionalContacts = MapAdditionalContacts(row), 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 List? 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? 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; } } } }