# 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": "", "dispatchVersion": "", "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 " \ -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.