mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-01 22:07:59 +00:00
155 lines
5.4 KiB
Markdown
155 lines
5.4 KiB
Markdown
|
|
# Fase 2 — Inline Edit + Concorrência
|
|||
|
|
|
|||
|
|
**Programa:** Work Orders Board
|
|||
|
|
**Objetivo:** Edição spreadsheet-style célula a célula no board SHOC, com dual RowVersion, audit granular e regras de domínio no backend.
|
|||
|
|
|
|||
|
|
**Depende de:** [Fase 0](../phase-0/README.md), [Fase 1](../phase-1/README.md)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Endpoints
|
|||
|
|
|
|||
|
|
### `PATCH /api/workorders/{id}/board`
|
|||
|
|
|
|||
|
|
Atualiza um único campo do board com validação otimista de concorrência.
|
|||
|
|
|
|||
|
|
**Autenticação:** Bearer JWT (`[Authorize]`)
|
|||
|
|
|
|||
|
|
**Request body:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"field": "scheduledDate",
|
|||
|
|
"value": "2026-06-25",
|
|||
|
|
"workOrderVersion": "<base64 RowVersion>",
|
|||
|
|
"dispatchVersion": "<base64 RowVersion | null para campos WO-only>",
|
|||
|
|
"primaryDispatchId": 123
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Campo | Obrigatório | Descrição |
|
|||
|
|
|-------|-------------|-----------|
|
|||
|
|
| `field` | Sim | Nome canônico do campo (case-insensitive) |
|
|||
|
|
| `value` | Sim* | Valor serializado como string (*pode ser vazio para limpar datas) |
|
|||
|
|
| `workOrderVersion` | Sim | `RowVersion` atual da WO (Base64) |
|
|||
|
|
| `dispatchVersion` | Condicional | Obrigatório para campos Dispatch quando dispatch já existe |
|
|||
|
|
| `primaryDispatchId` | Não | Valida que o dispatch pertence à WO |
|
|||
|
|
|
|||
|
|
**Responses:**
|
|||
|
|
|
|||
|
|
| HTTP | Body | Quando |
|
|||
|
|
|------|------|--------|
|
|||
|
|
| 200 | `WorkOrderBoardRowDto` | Sucesso — row completo com versões atualizadas |
|
|||
|
|
| 409 | `WorkOrderBoardConflictDto` + `currentState` | Conflito de versão |
|
|||
|
|
| 422 | `WorkOrderBoardValidationErrorDto` | Regra de domínio violada |
|
|||
|
|
| 400 | `Response` | Erro genérico / argumento inválido |
|
|||
|
|
|
|||
|
|
**Exemplo:**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
curl -X PATCH -H "Authorization: Bearer <token>" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"field":"siteCode","value":"BK5","workOrderVersion":"AQAAAAAAAAA="}' \
|
|||
|
|
"https://localhost:5001/api/workorders/1/board"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Campos editáveis
|
|||
|
|
|
|||
|
|
| field (API) | Agregado | Coluna board | Audit FieldName |
|
|||
|
|
|-------------|----------|--------------|-----------------|
|
|||
|
|
| `woNumber` | WorkOrder | WO# | InternalWONumber |
|
|||
|
|
| `workOrderType` | WorkOrder | Type | WorkOrderType |
|
|||
|
|
| `siteCode` | WorkOrder | Site | SiteCode |
|
|||
|
|
| `lifecycleStatus` | WorkOrder | Status | LifecycleStatus |
|
|||
|
|
| `assignTo` | WorkOrder | Assignee | AssignTo |
|
|||
|
|
| `dueDate` | WorkOrder | Due | DueDate |
|
|||
|
|
| `scheduledDate` | WorkOrder | Scheduled | ScheduledDate |
|
|||
|
|
| `targetWeek` | WorkOrder | Scheduled (week-only) | TargetWeek |
|
|||
|
|
| `scheduleWeekOnly` | WorkOrder | Scheduled | ScheduleWeekOnly |
|
|||
|
|
| `vendorId` | Dispatch | Vendor | VendorId |
|
|||
|
|
| `apptDate` | Dispatch | Appt | ApptDate |
|
|||
|
|
| `apptTime` | Dispatch/WO | Appt | ApptTime |
|
|||
|
|
| `docStatus` | WorkOrder | Doc | DocStatus |
|
|||
|
|
|
|||
|
|
**Read-only:** `isPastDue`, `carriedOver`, `rescheduleCount` (badge derivado; count incrementado por regra).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Dual RowVersion
|
|||
|
|
|
|||
|
|
| Agregado | Quando exigir versão |
|
|||
|
|
|----------|---------------------|
|
|||
|
|
| WorkOrder | Sempre (`workOrderVersion`) |
|
|||
|
|
| Dispatch | Campos vendor/appt quando dispatch primário já existe (`dispatchVersion`) |
|
|||
|
|
|
|||
|
|
O GET board retorna `rowVersion`, `dispatchRowVersion` e `primaryDispatchId` em cada row.
|
|||
|
|
|
|||
|
|
**409 currentState:** inclui row recarregado do DB para refresh imediato no FE.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Regras de domínio
|
|||
|
|
|
|||
|
|
| Regra | Comportamento |
|
|||
|
|
|-------|---------------|
|
|||
|
|
| Auto-schedule | `Incomplete` + `scheduledDate` + `assignTo` → `Scheduled` |
|
|||
|
|
| Reschedule | Mudança de `scheduledDate` com data anterior → `rescheduleCount++` |
|
|||
|
|
| Set-once tracking | Primeira `scheduledDate` → `OriginalDate` / `OriginalWeek` |
|
|||
|
|
| Past Due block | Não permite mudar `lifecycleStatus` quando `isPastDue` |
|
|||
|
|
| Cancel read-only | Status terminal (`Canceled`, `Closed`, `Complete`) bloqueia PATCH |
|
|||
|
|
| WO# 11 dígitos | Normalização numérica + unicidade |
|
|||
|
|
| Vendor ADR | Mutação em `Dispatch`; cria dispatch primário se ausente |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Audit
|
|||
|
|
|
|||
|
|
- 1 evento por campo alterado (`FieldChanged`, `StatusChanged`, ou `AssignmentChanged`)
|
|||
|
|
- Side effects (auto-schedule, rescheduleCount) geram eventos adicionais
|
|||
|
|
- Field lock (`WorkOrderFieldLocks`) criado automaticamente em edições manuais
|
|||
|
|
- Eventos Dispatch incluem `dispatchId`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Código entregue
|
|||
|
|
|
|||
|
|
| Camada | Arquivo |
|
|||
|
|
|--------|---------|
|
|||
|
|
| DTOs | `SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs` |
|
|||
|
|
| Helpers | `WorkOrderBoardMutationRules`, `WorkOrderNumberNormalizer`, `WorkOrderBoardFieldNames`, `WorkOrderBoardApptTimeParser` |
|
|||
|
|
| Update service | `SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs` |
|
|||
|
|
| Audit | `IWorkOrderAuditService.StageFieldChanged`, `LogFieldChangedAsync` |
|
|||
|
|
| API | `WorkOrderController` — `PATCH {id}/board` |
|
|||
|
|
| Testes | `WorkOrderBoardMutationRulesTests`, `WorkOrderNumberNormalizerTests`, `WorkOrderBoardUpdateServiceTests`, `WorkOrderBoardConcurrencyTests` |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Gates de aceite
|
|||
|
|
|
|||
|
|
### Desenvolvimento
|
|||
|
|
|
|||
|
|
- [x] `PATCH /api/workorders/{id}/board` para todos os campos da matriz
|
|||
|
|
- [x] Dual RowVersion validado; 409 com `currentState`
|
|||
|
|
- [x] Auto-schedule Incomplete→Scheduled
|
|||
|
|
- [x] RescheduleCount++ em reagendamento
|
|||
|
|
- [x] Bloqueio status quando PastDue; reagendar limpa flag on-read
|
|||
|
|
- [x] WO cancelado/closed rejeita edição (422)
|
|||
|
|
- [x] WO# normalizado 11 dígitos + unicidade
|
|||
|
|
- [x] Vendor/appt muta Dispatch; cria primário se ausente
|
|||
|
|
- [x] 1 audit event por campo; field lock criado
|
|||
|
|
- [x] 51 testes unitários total (25 Fase 2 + 26 Fases 0–1)
|
|||
|
|
|
|||
|
|
### Staging (pendente)
|
|||
|
|
|
|||
|
|
- [ ] Smoke Sync durante edição SHOC
|
|||
|
|
- [ ] UAT dispatcher: editar células reais no board
|
|||
|
|
- [ ] Latência PATCH aceitável (<200ms p95 Tier S)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Próximo passo
|
|||
|
|
|
|||
|
|
**Fase 3** — Criação wizard/inline, soft cancel dedicado, ManualEditFlag na criação.
|