mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-06 11:12:08 +00:00
fix(work-orders): address PR13 phase-1 review feedback
This commit is contained in:
parent
cc8825ace0
commit
f3d2a5cfe7
6 changed files with 198 additions and 3 deletions
|
|
@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.Forms;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.VisualBasic;
|
using Microsoft.VisualBasic;
|
||||||
|
|
@ -83,7 +84,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
|
|
||||||
[HttpGet("board")]
|
[HttpGet("board")]
|
||||||
public async Task<IActionResult> GetBoard(
|
public async Task<IActionResult> GetBoard(
|
||||||
[FromQuery] DateOnly weekStart,
|
[FromQuery, BindRequired] DateOnly weekStart,
|
||||||
[FromQuery] DateOnly? weekEnd = null,
|
[FromQuery] DateOnly? weekEnd = null,
|
||||||
[FromQuery] List<string>? dispatchers = null,
|
[FromQuery] List<string>? dispatchers = null,
|
||||||
[FromQuery] bool myWorkOrders = false,
|
[FromQuery] bool myWorkOrders = false,
|
||||||
|
|
|
||||||
21
Api.SeaHavenIndustries/Filters/ArgumentExceptionFilter.cs
Normal file
21
Api.SeaHavenIndustries/Filters/ArgumentExceptionFilter.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Filters
|
||||||
|
{
|
||||||
|
public class ArgumentExceptionFilter : IExceptionFilter
|
||||||
|
{
|
||||||
|
public void OnException(ExceptionContext context)
|
||||||
|
{
|
||||||
|
if (context.Exception is not ArgumentException ex) return;
|
||||||
|
|
||||||
|
context.Result = new BadRequestObjectResult(new
|
||||||
|
{
|
||||||
|
status = "Error",
|
||||||
|
message = ex.Message,
|
||||||
|
code = 400
|
||||||
|
});
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
|
||||||
builder.Services.AddControllers(options =>
|
builder.Services.AddControllers(options =>
|
||||||
{
|
{
|
||||||
options.Filters.Add<Api.SeaHavenIndustries.Filters.ConcurrencyExceptionFilter>();
|
options.Filters.Add<Api.SeaHavenIndustries.Filters.ConcurrencyExceptionFilter>();
|
||||||
|
options.Filters.Add<Api.SeaHavenIndustries.Filters.ArgumentExceptionFilter>();
|
||||||
}).AddJsonOptions(options =>
|
}).AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
options.JsonSerializerOptions.MaxDepth = 264; // or any other desired value
|
options.JsonSerializerOptions.MaxDepth = 264; // or any other desired value
|
||||||
|
|
|
||||||
14
Data.SeaHavenIndustries/Enums/LifecycleStatusSets.cs
Normal file
14
Data.SeaHavenIndustries/Enums/LifecycleStatusSets.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace Data.SeaHavenIndustries.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shared terminal lifecycle statuses for EF-translatable filters and board rules.
|
||||||
|
/// </summary>
|
||||||
|
public static class LifecycleStatusSets
|
||||||
|
{
|
||||||
|
public static readonly LifecycleStatus?[] Terminal =
|
||||||
|
{
|
||||||
|
LifecycleStatus.Completed,
|
||||||
|
LifecycleStatus.Canceled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,8 +33,7 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
|
|
||||||
var unscheduledBase = baseQuery.Where(w =>
|
var unscheduledBase = baseQuery.Where(w =>
|
||||||
w.ScheduledDate == null
|
w.ScheduledDate == null
|
||||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
&& !LifecycleStatusSets.Terminal.Contains(w.LifecycleStatus));
|
||||||
&& w.LifecycleStatus != LifecycleStatus.Canceled);
|
|
||||||
|
|
||||||
var scheduledTotal = await scheduledBase.CountAsync();
|
var scheduledTotal = await scheduledBase.CountAsync();
|
||||||
|
|
||||||
|
|
|
||||||
159
docs/work-orders/phase-1/README.md
Normal file
159
docs/work-orders/phase-1/README.md
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
# Fase 1 — Weekly Board (leitura)
|
||||||
|
|
||||||
|
**Programa:** Work Orders Board
|
||||||
|
**Objetivo:** Endpoint window-based para carregar a visão semanal do SHOC (Mon–Fri + Unscheduled) com projeção das 14 colunas operacionais.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### `GET /api/workorders/board`
|
||||||
|
|
||||||
|
Retorna WOs agendadas na semana + seção **Unscheduled** em uma única chamada.
|
||||||
|
|
||||||
|
**Autenticação:** Bearer JWT (`[Authorize]`)
|
||||||
|
|
||||||
|
**Query params:**
|
||||||
|
|
||||||
|
| Param | Obrigatório | Descrição |
|
||||||
|
|-------|-------------|-----------|
|
||||||
|
| `weekStart` | Sim (`BindRequired`) | Segunda-feira da semana (`YYYY-MM-DD`). Omitir retorna **400**. |
|
||||||
|
| `weekEnd` | Não | Default: `weekStart + 4 dias` (sexta). Janela pode ter até 7 dias. |
|
||||||
|
| `dispatchers` | Não | Lista de GUIDs; use `__unassigned__` para não atribuídos. **Ignorado** quando `myWorkOrders=true`. |
|
||||||
|
| `myWorkOrders` | Não | `true` → filtra `AssignTo == usuário logado` e tem **precedência** sobre `dispatchers` (não é AND). |
|
||||||
|
| `types` | Não | Valores enum `WorkOrderType` (`PM`, `PO`, `Emergency`, etc.) |
|
||||||
|
| `search` | Não | Busca contextual (site, WO#, location, dispatcher, trade, vendor, status) |
|
||||||
|
|
||||||
|
**Exemplo:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <token>" \
|
||||||
|
"https://localhost:5001/api/workorders/board?weekStart=2026-06-22&myWorkOrders=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"weekStart": "2026-06-22",
|
||||||
|
"weekEnd": "2026-06-26",
|
||||||
|
"counts": { "returned": 42, "total": 58 },
|
||||||
|
"unscheduled": [ /* WorkOrderBoardRowDto[] */ ],
|
||||||
|
"scheduled": [ /* WorkOrderBoardRowDto[] — agrupar por dayGroup no FE */ ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `counts.total` — WOs agendadas na semana (filtros dispatcher/tipo, **sem** search)
|
||||||
|
- `counts.returned` — WOs agendadas após aplicar search
|
||||||
|
- `dayGroup` — `monday`…`friday` para dias úteis; **`null` no sábado/domingo** (a janela pode incluir fim de semana; o FE deve tratar `dayGroup` nulo)
|
||||||
|
- Referência de dia / past-due: **UTC** nesta fase (timezone de negócio fica para fase posterior)
|
||||||
|
|
||||||
|
### `GET /api/workorders/lookups/dispatchers`
|
||||||
|
|
||||||
|
Lista opções para o filtro/dropdown de assignee do board.
|
||||||
|
|
||||||
|
**Escopo atual (placeholder):** todos os users com `IsDeleted != true` — ainda **sem** filtro por role. Quando roles de assignee estiverem definidos, este lookup será restrito.
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "id": "guid", "name": "Jane Doe", "initials": "JD", "color": "#4A90D9" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Matriz coluna → campo API
|
||||||
|
|
||||||
|
| Coluna board | Campos response |
|
||||||
|
|--------------|-----------------|
|
||||||
|
| WO# | `woNumber`, `rescheduleCount`, `carriedOver` |
|
||||||
|
| Type | `workOrderType`, `isPastDue` |
|
||||||
|
| Site | `siteCode`, `locationName`, `pocName`, `pocPhone`, `pocNotes` |
|
||||||
|
| Status | `lifecycleStatus`, `lifecycleStatusLabel`, `legacyStatus` |
|
||||||
|
| Assignee | `dispatcherId`, `dispatcherName`, `initials`, `color` |
|
||||||
|
| Due | `dueDate` |
|
||||||
|
| Scheduled | `scheduledDate`, `targetWeek`, `scheduleWeekOnly`, `dayGroup` |
|
||||||
|
| Vendor | `vendorId`, `vendorName`, `techName`, `techPhone` |
|
||||||
|
| Appt | `apptDate`, `apptTime` |
|
||||||
|
| Past Due | `isPastDue` (derivado on-read) |
|
||||||
|
| Carried | `carriedOver` |
|
||||||
|
| Reschedule | `rescheduleCount` |
|
||||||
|
| Doc | `docStatus` |
|
||||||
|
| Actions | FE only |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regras de derivação
|
||||||
|
|
||||||
|
### isPastDue
|
||||||
|
|
||||||
|
```
|
||||||
|
ScheduledDate < UTC hoje
|
||||||
|
AND LifecycleStatus NOT IN (Complete, Canceled, Closed)
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementação: `SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs`
|
||||||
|
|
||||||
|
### Vendor / Appt
|
||||||
|
|
||||||
|
Projeção read-only via `WorkOrder.PrimaryDispatchId` → `Dispatch` → `Vendor` ([ADR](../phase-0/adr-vendor-source-of-truth.md)).
|
||||||
|
|
||||||
|
### Janela semanal
|
||||||
|
|
||||||
|
WO entra em **scheduled** se:
|
||||||
|
|
||||||
|
- `ScheduledDate` entre `weekStart` e `weekEnd`, **ou**
|
||||||
|
- `ScheduleWeekOnly == true` e `TargetWeek == weekStart`
|
||||||
|
|
||||||
|
**Unscheduled:** `ScheduledDate == null` e status não terminal (`LifecycleStatusSets.Terminal`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Código entregue
|
||||||
|
|
||||||
|
| Camada | Arquivo |
|
||||||
|
|--------|---------|
|
||||||
|
| DTOs | `SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs` |
|
||||||
|
| Derived fields | `SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs` |
|
||||||
|
| Data | `SeaHaven.DataServices/Implementation/WorkOrderBoardDataService.cs` |
|
||||||
|
| Service | `SeaHaven.Services/Implementation/WorkOrderBoardService.cs` |
|
||||||
|
| API | `Api.SeaHavenIndustries/Controllers/WorkOrderController.cs` (`board`, `lookups/dispatchers`) |
|
||||||
|
| Migration | `Data.SeaHavenIndustries/Migrations/20260701120000_Phase1_BoardIndexes.cs` |
|
||||||
|
| Testes | `SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs`, `WorkOrderBoardServiceTests.cs` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gates de aceite
|
||||||
|
|
||||||
|
### Desenvolvimento (concluído)
|
||||||
|
|
||||||
|
- [x] Endpoint `GET board` — semana + Unscheduled
|
||||||
|
- [x] Projeção 12/14 colunas de dados
|
||||||
|
- [x] `isPastDue` derivado on-read
|
||||||
|
- [x] Vendor via dispatch primário
|
||||||
|
- [x] Filtros dispatcher / My WOs / tipo / search
|
||||||
|
- [x] Contador X of Y
|
||||||
|
- [x] Índices Tier S
|
||||||
|
- [x] 14 testes unitários board (+ 12 Fase 0 = 26 total)
|
||||||
|
|
||||||
|
### Staging / produção (pendente)
|
||||||
|
|
||||||
|
- [ ] Sign-off CTO/PO (herda gates Fase 0)
|
||||||
|
- [ ] Dry-run migration Phase0 + Phase1 em staging
|
||||||
|
- [ ] Tier volume confirmado em staging/prod
|
||||||
|
- [ ] Smoke checklist Blazor / Sync / Portal
|
||||||
|
- [ ] Gate Vendor Portal regression (endpoints dispatch inalterados)
|
||||||
|
- [ ] Latência aceitável com volume real (Tier S: < 25k WOs)
|
||||||
|
- [ ] Feature flag SHOC board em staging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coexistência
|
||||||
|
|
||||||
|
Endpoints legados (`GetWorkOrderList`, `GetWorkorderById`, etc.) **não foram alterados**. O board usa contrato novo em rotas separadas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Próximo passo
|
||||||
|
|
||||||
|
**Fase 2** — Inline edit + concorrência (dual RowVersion, audit por campo, PATCH granular).
|
||||||
Loading…
Add table
Reference in a new issue