mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 11:53:12 +00:00
Stop client writes from changing Locations.AccountId, make the SH-221 migration discoverable, and thread the board-create CancellationToken through lookup and persistence.
253 lines
9.7 KiB
C#
253 lines
9.7 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,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
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, User, userId, cancellationToken);
|
||
return Ok(result);
|
||
}
|
||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
||
{
|
||
return StatusCode(StatusCodes.Status403Forbidden, new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
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, User, userId);
|
||
return Ok(result);
|
||
}
|
||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
||
{
|
||
return StatusCode(StatusCodes.Status403Forbidden, new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
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,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
try
|
||
{
|
||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
var row = await _boardCreateService.CreateAsync(request, User, actorId, cancellationToken);
|
||
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 == "Forbidden")
|
||
{
|
||
return StatusCode(StatusCodes.Status403Forbidden, new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
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, User, actorId);
|
||
return Ok(row);
|
||
}
|
||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
||
{
|
||
return StatusCode(StatusCodes.Status403Forbidden, 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
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|