shoc-backend/SeaHaven.DataServices/Implementation/WorkOrderBoardDataService.cs
Arthur Bassi 11fed06f03 feat(work-orders): add Phase 4 board search and advanced search API
Harden contextual search on the weekly board and expose paginated cross-week GET /board/search with date presets, FE filter params, SQL indexes, and unit tests.
2026-07-24 09:58:39 -03:00

95 lines
3.9 KiB
C#

using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Helpers;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class WorkOrderBoardDataService : IWorkOrderBoardDataService
{
private readonly ApplicationDbContext _context;
public WorkOrderBoardDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<WorkOrderBoardQueryResult> GetBoardRowsAsync(WorkOrderBoardQuery query)
{
var weekStartDate = query.WeekStart.ToDateTime(TimeOnly.MinValue);
var weekEndDate = query.WeekEnd.ToDateTime(TimeOnly.MinValue);
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
baseQuery = WorkOrderBoardQueryFilters.ApplyDispatcherFilter(
baseQuery, query.Dispatchers, query.MyWorkOrders, query.CurrentUserId);
baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types);
var scheduledBase = baseQuery.Where(w =>
(w.ScheduledDate != null
&& w.ScheduledDate.Value.Date >= weekStartDate.Date
&& w.ScheduledDate.Value.Date <= weekEndDate.Date)
|| (w.ScheduleWeekOnly == true && w.TargetWeek == query.WeekStart));
var unscheduledBase = baseQuery.Where(w =>
w.ScheduledDate == null
&& !LifecycleStatusSets.Terminal.Contains(w.LifecycleStatus));
var scheduledTotal = await scheduledBase.CountAsync();
var normalizedSearch = WorkOrderBoardSearchFilter.NormalizeSearch(query.Search);
if (normalizedSearch != null)
{
scheduledBase = WorkOrderBoardSearchFilter.Apply(scheduledBase, normalizedSearch);
unscheduledBase = WorkOrderBoardSearchFilter.Apply(unscheduledBase, normalizedSearch);
}
var scheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync(
scheduledBase.OrderBy(w => w.ScheduledDate).ThenBy(w => w.InternalWONumber),
isUnscheduled: false);
var unscheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync(
unscheduledBase.OrderBy(w => w.InternalWONumber),
isUnscheduled: true);
return new WorkOrderBoardQueryResult(scheduledRows, unscheduledRows, scheduledTotal);
}
public async Task<IReadOnlyList<DispatcherLookupRow>> GetDispatcherLookupsAsync()
{
return await _context.Users
.AsNoTracking()
.Where(u => u.IsDeleted != true)
.OrderBy(u => u.FirstName)
.ThenBy(u => u.LastName)
.Select(u => new DispatcherLookupRow(
u.Id,
u.FirstName ?? "",
u.LastName,
u.Initials,
u.Color))
.ToListAsync();
}
public async Task<WorkOrderBoardRawRow?> GetBoardRowByIdAsync(int workOrderId)
{
var query = _context.workOrders
.AsNoTracking()
.Where(w => w.Id == workOrderId && w.istemplate != true && (w.IsDeleted != true || w.IsDeleted == null));
var rows = await WorkOrderBoardProjection.ProjectRowsAsync(query, isUnscheduled: false);
return rows.FirstOrDefault();
}
public async Task<bool> InternalWoNumberExistsAsync(string normalizedWoNumber, int excludeWorkOrderId)
{
return await _context.workOrders
.AsNoTracking()
.AnyAsync(w =>
w.Id != excludeWorkOrderId
&& w.istemplate != true
&& (w.IsDeleted != true || w.IsDeleted == null)
&& w.InternalWONumber == normalizedWoNumber);
}
}
}