mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 15:23:12 +00:00
* feat(work-orders): enrich board search overdue filters and 0-based paging * fix(work-orders): align stacked services with CI build * fix(tests): pass userDataService in comment service unit test * fix(work-orders): use dedicated overdue query flag Stop treating WorkOrderType.Other as an overdue sentinel. Board and advanced search now accept overdue=true while types=Other filters real Other rows; combining both uses OR. * test(work-orders): cover overdue date/status boundary and Other type-filter Lock the PR #23 overdue regression boundary through the public advanced search service. Prove overdue filtering is driven by past-due date plus non-terminal status, not by the WorkOrderType.Other sentinel: - Other + future/not-completed excluded from overdue - past-due + Scheduled included; past-due + Completed/Canceled excluded - types=[PM, Other] keeps real Other rows and does not pull past-due rows - assert 0-based paging (Page=0) is preserved alongside overdue/type filters --------- Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com> Co-authored-by: Alexandre Brandizzi <alex_brandizzi@hotmail.com>
218 lines
8.2 KiB
C#
218 lines
8.2 KiB
C#
using Api.SeaHavenIndustries.DTOs;
|
||
using Api.SeaHavenIndustries.Helper;
|
||
using Data.SeaHavenIndustries;
|
||
using Data.SeaHavenIndustries.Enums;
|
||
using FluentValidation;
|
||
using Microsoft.AspNetCore.Authorization;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||
using SeaHaven.Services.DTOs;
|
||
using SeaHaven.Services.Exceptions;
|
||
using SeaHaven.Services.Helpers;
|
||
using SeaHaven.Services.Interfaces;
|
||
using System.Security.Claims;
|
||
|
||
namespace Api.SeaHavenIndustries.Controllers
|
||
{
|
||
[Authorize]
|
||
[ApiController]
|
||
[Route("api/WorkOrder")]
|
||
[Route("api/workorders")]
|
||
public class WorkOrderBoardController : Controller
|
||
{
|
||
private readonly IWorkOrderBoardService _workOrderBoardService;
|
||
private readonly IWorkOrderBoardUpdateService _boardUpdateService;
|
||
private readonly IWorkOrderBoardCreateService _boardCreateService;
|
||
private readonly IWorkOrderBoardCancelService _boardCancelService;
|
||
private readonly IWorkOrderAdvancedSearchService _advancedSearchService;
|
||
|
||
public WorkOrderBoardController(
|
||
IWorkOrderBoardService workOrderBoardService,
|
||
IWorkOrderBoardUpdateService boardUpdateService,
|
||
IWorkOrderBoardCreateService boardCreateService,
|
||
IWorkOrderBoardCancelService boardCancelService,
|
||
IWorkOrderAdvancedSearchService advancedSearchService)
|
||
{
|
||
_workOrderBoardService = workOrderBoardService;
|
||
_boardUpdateService = boardUpdateService;
|
||
_boardCreateService = boardCreateService;
|
||
_boardCancelService = boardCancelService;
|
||
_advancedSearchService = advancedSearchService;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Weekly board payload.
|
||
/// When <paramref name="myWorkOrders"/> is true, the authenticated user filter wins and
|
||
/// <paramref name="dispatchers"/> is ignored (not AND-combined).
|
||
/// Weekend WOs in a Mon–Sun window may have <c>dayGroup = null</c>; FE should handle that.
|
||
/// </summary>
|
||
[HttpGet("board")]
|
||
public async Task<IActionResult> GetBoard(
|
||
[FromQuery, BindRequired] DateOnly weekStart,
|
||
[FromQuery] DateOnly? weekEnd = null,
|
||
[FromQuery] List<string>? dispatchers = null,
|
||
[FromQuery] bool myWorkOrders = false,
|
||
[FromQuery] List<WorkOrderType>? types = null,
|
||
[FromQuery] bool overdue = false,
|
||
[FromQuery] string? search = null)
|
||
{
|
||
var resolvedWeekEnd = weekEnd ?? weekStart.AddDays(4);
|
||
var weekValidationError = WorkOrderOperationalWeek.ValidateWeekWindow(weekStart, resolvedWeekEnd);
|
||
if (weekValidationError != null)
|
||
return BadRequest(weekValidationError);
|
||
|
||
try
|
||
{
|
||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
var query = new WorkOrderBoardQueryDto
|
||
{
|
||
WeekStart = weekStart,
|
||
WeekEnd = weekEnd,
|
||
Dispatchers = dispatchers,
|
||
MyWorkOrders = myWorkOrders,
|
||
Types = types,
|
||
Overdue = overdue,
|
||
Search = search
|
||
};
|
||
|
||
var result = await _workOrderBoardService.GetBoardAsync(query, userId);
|
||
return Ok(result);
|
||
}
|
||
catch (ArgumentException ex)
|
||
{
|
||
return BadRequest(ex.Message);
|
||
}
|
||
}
|
||
|
||
[HttpGet("board/search")]
|
||
public async Task<IActionResult> SearchBoard([FromQuery] WorkOrderAdvancedSearchQueryDto query)
|
||
{
|
||
try
|
||
{
|
||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
var result = await _advancedSearchService.SearchAsync(query, userId);
|
||
return Ok(result);
|
||
}
|
||
catch (ArgumentException ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Dispatcher dropdown options. Currently returns all non-deleted users
|
||
/// (role-filtered assignee list deferred until roles settle).
|
||
/// </summary>
|
||
[HttpGet("lookups/dispatchers")]
|
||
public async Task<IActionResult> GetDispatcherLookups()
|
||
{
|
||
var result = await _workOrderBoardService.GetDispatcherLookupsAsync();
|
||
return Ok(result);
|
||
}
|
||
|
||
[HttpPatch("{id:int}/board")]
|
||
public async Task<IActionResult> PatchBoardField(int id, [FromBody] WorkOrderBoardPatchRequestDto request)
|
||
{
|
||
try
|
||
{
|
||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
var row = await _boardUpdateService.PatchFieldAsync(id, request, actorId);
|
||
return Ok(row);
|
||
}
|
||
catch (WorkOrderBoardConcurrencyException ex)
|
||
{
|
||
return Conflict(new WorkOrderBoardConflictDto
|
||
{
|
||
CurrentState = ex.CurrentState
|
||
});
|
||
}
|
||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||
{
|
||
return NotFound(new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
catch (WorkOrderBoardValidationException ex)
|
||
{
|
||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
catch (ArgumentException ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
[HttpPost("board")]
|
||
public async Task<IActionResult> CreateBoardWorkOrder([FromBody] WorkOrderBoardCreateRequestDto request)
|
||
{
|
||
try
|
||
{
|
||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
var row = await _boardCreateService.CreateAsync(request, actorId);
|
||
return Ok(row);
|
||
}
|
||
catch (ValidationException vex)
|
||
{
|
||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||
}
|
||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "DuplicateWoNumber")
|
||
{
|
||
return Conflict(new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||
{
|
||
return NotFound(new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
catch (WorkOrderBoardValidationException ex)
|
||
{
|
||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
}
|
||
|
||
[HttpPost("{id:int}/cancel")]
|
||
public async Task<IActionResult> CancelBoardWorkOrder(int id)
|
||
{
|
||
try
|
||
{
|
||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
var row = await _boardCancelService.CancelAsync(id, actorId);
|
||
return Ok(row);
|
||
}
|
||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||
{
|
||
return NotFound(new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
catch (WorkOrderBoardValidationException ex)
|
||
{
|
||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|