mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
chore: remove docs and scripts from phase 2 PR scope
This commit is contained in:
parent
88caede86a
commit
25eb6293e0
35 changed files with 0 additions and 4448 deletions
|
|
@ -1,194 +0,0 @@
|
|||
# ADR: Work Orders Board API (Fase 1 read-only)
|
||||
|
||||
**Status:** Proposed — spikes G0, G1, G3 concluídas
|
||||
**Data:** 2026-06-19
|
||||
**Deciders:** Backend + FE (aprovação Fase 1 pending)
|
||||
|
||||
---
|
||||
|
||||
## Contexto
|
||||
|
||||
O Schedule Board (FE) organiza work orders por `ScheduledDate` em visão semanal. O backend atual expõe `GetWorkOrderList` — listagem CRUD paginada sem janela temporal, sem `siteCode`, sem vendor inline, sem `ScheduledDate` na projeção.
|
||||
|
||||
Spikes de referência:
|
||||
|
||||
- [Consumer Audit](../spikes/consumer-audit-getworkorderlist.md)
|
||||
- [WorkOrderType](../spikes/work-order-type.md)
|
||||
- [Search Strategy](../spikes/search-strategy.md)
|
||||
|
||||
---
|
||||
|
||||
## Decisão
|
||||
|
||||
### 1. Endpoint dedicado (sem `projection=`)
|
||||
|
||||
Criar endpoint **separado** para o Schedule Board. **Não alterar** `GetWorkOrderList`.
|
||||
|
||||
```http
|
||||
GET /api/workorders/GetWorkOrdersBoard
|
||||
?scheduledFrom=2026-06-01
|
||||
&scheduledTo=2026-06-07
|
||||
&assignee=guid1&assignee=guid2
|
||||
&search=BK5
|
||||
&status=Open&status=InProgress
|
||||
&locationId=42
|
||||
```
|
||||
|
||||
Rota alias: `GET /api/WorkOrder/GetWorkOrdersBoard` (consistente com controller existente).
|
||||
|
||||
**Rejeitado:**
|
||||
|
||||
- `GET /api/workorders/table`
|
||||
- `GET /GetWorkOrderList?projection=table`
|
||||
|
||||
### 2. Window-based fetch (sem paginação Fase 1)
|
||||
|
||||
| Abordagem | Fase 1 |
|
||||
|-----------|--------|
|
||||
| `scheduledFrom` + `scheduledTo` | **Obrigatório** |
|
||||
| `page` / `pageSize` | **Ausente** |
|
||||
| Limite janela | Max **90 dias** (validação server-side) |
|
||||
|
||||
Response envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"scheduledFrom": "2026-06-01",
|
||||
"scheduledTo": "2026-06-07",
|
||||
"totalCount": 42,
|
||||
"items": [
|
||||
{
|
||||
"row": { /* WorkOrderBoardRowDto */ },
|
||||
"presentation": { /* BoardRowPresentation */ }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. DTO — domínio vs presentation
|
||||
|
||||
**`WorkOrderBoardRowDto`** — dados persistidos / joináveis:
|
||||
|
||||
```csharp
|
||||
public record WorkOrderBoardRowDto(
|
||||
int Id,
|
||||
string? WoNumber,
|
||||
string? SiteCode,
|
||||
string? LocationLabel,
|
||||
DateOnly? ScheduledDate,
|
||||
TimeOnly? ScheduledStart,
|
||||
DateOnly? DueDate,
|
||||
string? WorkOrderType,
|
||||
string? ServiceType,
|
||||
string? AssigneeId,
|
||||
string? AssigneeName,
|
||||
string? VendorCompany,
|
||||
string? VendorTechnician,
|
||||
string Status,
|
||||
string? PocName,
|
||||
string? PocPhone
|
||||
);
|
||||
```
|
||||
|
||||
**`BoardRowPresentation`** — calculado no mapper backend:
|
||||
|
||||
```csharp
|
||||
public record BoardRowPresentation(
|
||||
string StatusDisplay,
|
||||
bool IsPastDue,
|
||||
string? WorkOrderTypeDisplay
|
||||
);
|
||||
```
|
||||
|
||||
Mapper: `IWorkOrderBoardMapper` centraliza Past Due e type display.
|
||||
|
||||
### 4. Query EF (Fase 1)
|
||||
|
||||
- Filtro base: `istemplate != true`
|
||||
- Janela: `ScheduledDate >= from && ScheduledDate < to.AddDays(1)`
|
||||
- Includes: `Locations`, `AssignToUser`, `Dispatches` → `Vendor` (1 vendor por WO — first active dispatch)
|
||||
- POC: `WorkOrderContacts` (primary)
|
||||
- Service type: `Trade` ou category join (mínimo: `Trade`)
|
||||
- **Não incluir** subquery `lastUpdated` (Comments/AuditLog)
|
||||
|
||||
### 5. Search (conforme spike)
|
||||
|
||||
- Search scoped à janela temporal
|
||||
- Servidor: `StartsWith` em `InternalWONumber`, `SiteCode`
|
||||
- Count ≤ **300**: retornar semana inteira; FE filtra title/location/serviceType
|
||||
- Count > 300: exigir termo indexável ou retornar 400
|
||||
- Assignee: `assigneeId[]` multi — **não** filtrar por nome
|
||||
|
||||
Índices (migration separada pós-aprovação):
|
||||
|
||||
- `IX_workOrders_ScheduledDate` (P1)
|
||||
- `IX_workOrders_InternalWONumber`, `IX_workOrders_SiteCode` (P2, após fix tamanho coluna)
|
||||
|
||||
### 6. WorkOrderType
|
||||
|
||||
- Mapear coluna legada `WorkOrderType varchar(50)` na entidade EF
|
||||
- Expor em `WorkOrderBoardRowDto`
|
||||
- `IsPastDue` em presentation — **não** confundir com tipo
|
||||
- Enum C# adiado até amostragem prod
|
||||
|
||||
### 7. Status mapping
|
||||
|
||||
- API retorna status **canônico** (`Open`, `InProgress`, `Completed`, `Cancelled`, `OnHold`)
|
||||
- `StatusDisplay` via `IWorkOrderStatusMapper` (workshop G2 separado)
|
||||
- Sem migration de enum DB
|
||||
|
||||
---
|
||||
|
||||
## Consequências
|
||||
|
||||
### Positivas
|
||||
|
||||
- 1 HTTP call por mudança de semana (FE)
|
||||
- Contrato estável para board sem acoplar CRUD legado
|
||||
- Performance previsível (janela + threshold 300)
|
||||
- Blazor legado inalterado (EF local)
|
||||
|
||||
### Negativas / trade-offs
|
||||
|
||||
- Duplicação parcial de lógica de listagem vs `GetWorkOrderList` (aceitável — bounded contexts distintos)
|
||||
- Vendor technician pode ser null Fase 1 se dispatch spike incompleto
|
||||
- Setup DB local necessário para validação integrada
|
||||
|
||||
---
|
||||
|
||||
## Compliance com spikes
|
||||
|
||||
| Spike | Gate | Resultado |
|
||||
|-------|------|-----------|
|
||||
| Consumer Audit | G0 | `GetWorkOrderList` órfão no repo — endpoint dedicado aprovado |
|
||||
| WorkOrderType | G1 | Mapear coluna EF; Overdue = presentation |
|
||||
| Search Strategy | G3 | Window-first, threshold 300, StartsWith identificadores |
|
||||
|
||||
---
|
||||
|
||||
## Fora de escopo Fase 1
|
||||
|
||||
- PATCH inline edit
|
||||
- Completion doc, media, flags, reorder
|
||||
- Search vendor/tech server-side
|
||||
- Paginação cross-week
|
||||
- Alteração de `GetWorkOrderList`
|
||||
|
||||
---
|
||||
|
||||
## Próximos passos (implementação)
|
||||
|
||||
1. Aprovação explícita **Fase 1 only**
|
||||
2. Migration: `WorkOrderType` + índice `ScheduledDate`
|
||||
3. `GetWorkOrdersBoard` em `WorkOrderController` + `WorkOrderDataService`
|
||||
4. DTOs + `IWorkOrderBoardMapper`
|
||||
5. FE: `useWorkOrdersBoard({ from, to })` substituindo `RAW_ORDERS`
|
||||
|
||||
---
|
||||
|
||||
## Referências
|
||||
|
||||
- Gap analysis v3: `work_orders_api_gap_analysis_ce735946.plan.md`
|
||||
- [`WorkOrderController.cs`](../../Api.SeaHavenIndustries/Controllers/WorkOrderController.cs)
|
||||
- [`WorkOrderDataService.cs`](../../SeaHaven.DataServices/Implementation/WorkOrderDataService.cs)
|
||||
- [`DispatchController.List`](../../Api.SeaHavenIndustries/Controllers/DispatchController.cs) — padrão janela temporal
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
# Auditoria — API Work Orders vs Frontend (WorkOrders.tsx)
|
||||
|
||||
**Data:** 2026-06-19
|
||||
**Fonte da verdade (FE):** `seahaven.desing/src/pages/WorkOrders.tsx`
|
||||
**Backend auditado:** `seaheven.api`
|
||||
|
||||
---
|
||||
|
||||
## Resumo executivo
|
||||
|
||||
| Métrica | Valor |
|
||||
|---------|-------|
|
||||
| Compatibility Score | **24/100** |
|
||||
| Backend Readiness | **Not Ready** |
|
||||
| Confidence | **High** |
|
||||
|
||||
A API atual foi construída para **CRUD legado** (listagem paginada, detalhe rico, dispatch). O frontend `WorkOrders.tsx` define um **Schedule Board operacional** (visão semanal, 14 colunas, edição inline, advanced search, completion doc WO-level). Os contratos são fundamentalmente diferentes.
|
||||
|
||||
**O que funciona hoje:** id, dueDate, location (parcial), CRUD básico, detalhe rico, comments, audit field-based, mídia WO-level, reatribuição, dispatch.
|
||||
|
||||
**O que falta:** listagem window-based, 14 colunas board, PATCH inline, campos derivados, advanced search, completion doc WO-level, jobs Past Due/Carried Over, status 10 labels, WO# 11 dígitos.
|
||||
|
||||
---
|
||||
|
||||
## Endpoints que existem hoje
|
||||
|
||||
| Endpoint | O que entrega |
|
||||
|----------|----------------|
|
||||
| `GET GetWorkOrderList` | Lista **paginada** (default 12): id, número, título, location, priority, status, dueDate, assignedTo, lastUpdated |
|
||||
| `GET Getworkorders` / `GetworkordersDD` | Lista legada para admin/dropdown |
|
||||
| `GET GetFilteredWorkorder` / `2` | Filtros legados (assignee, location, priority, status, due date buckets) — carrega tudo em memória |
|
||||
| `GET GetWorkorderById` / `{id}` | Detalhe: título, descrição, datas, status, problem, trade, POC, attachments, comments, audit, dispatches |
|
||||
| `POST AddWorkorder` | Criação via form: título, descrição, assignee, dueDate, location, priority, mídia, contacts/categories |
|
||||
| `POST/PUT EditWorkorder` | Update parcial via form (subset mapeado ao service) |
|
||||
| `POST ChangeStatus` | Muda status (string livre) + audit |
|
||||
| `POST ChangeAssignment` | Reatribui dispatcher + audit |
|
||||
| `POST AddComment` / `AddCommentJson` | Comentário + upload opcional |
|
||||
| `GET GetComments` / `GetCommentsByWorkorderId` | Lista comentários |
|
||||
| `DELETE DeleteWorkorder` | **Delete físico** (não soft cancel) |
|
||||
| Dispatch endpoints | Vendor, checklist, signoff, verify — **nível dispatch**, não board |
|
||||
|
||||
**Lookups relacionados (outros controllers):** `Common/GetUsers`, `Location/GetLocationList`, `Vendor/GetVendorList` — paginados, contrato diferente do FE.
|
||||
|
||||
**Documentado mas não implementado no código:** `GET GetWorkOrdersBoard`, `IWorkOrderBoardMapper`, `IWorkOrderStatusMapper` (ver `docs/adr/work-orders-board-api.md`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Listagem da tela principal (visão semanal)
|
||||
|
||||
| Frontend espera | API entrega hoje | Gap |
|
||||
|-----------------|------------------|-----|
|
||||
| 1 call por semana (`weekStart` / `weekEnd`) | Paginação `page` / `pageSize` sem janela temporal | Sem fetch window-based |
|
||||
| WOs agrupados por dia (Mon–Fri) | Lista flat paginada | Sem `scheduledDate` na projeção de listagem |
|
||||
| Seção **Unscheduled** sempre visível no topo | Só WOs da página atual | Sem WOs sem data retornados independente da semana |
|
||||
| `targetWeek` (scheduling week-only) | Campo inexistente na entidade/API | Não implementado |
|
||||
| Contador `X of Y` da semana ativa | `totalCount` global paginado | Contagem inadequada para o board |
|
||||
| Skeleton Mon–Fri vazios | N/A (FE-only) | OK — responsabilidade do FE |
|
||||
|
||||
**Evidência BE:** `WorkOrderDataService.GetWorkOrderListPagedAsync` — projeção L163-183, sem `ScheduledDate`, `SiteCode`, vendor, type.
|
||||
|
||||
---
|
||||
|
||||
## 2. Colunas da tabela (`COLS`)
|
||||
|
||||
Definidas no FE em `WorkOrders.tsx` L4234-4249.
|
||||
|
||||
| Coluna FE | Campo(s) FE | API hoje | Gap |
|
||||
|-----------|-------------|----------|-----|
|
||||
| Grip | drag reorder | Ausente | FE local-only; BE não persiste ordem |
|
||||
| Flag | flag pessoal | Ausente | FE session-only |
|
||||
| **SITE** | `site`, `pocName`, `pocPhone`, `pocNotes` | List: `location` (name); Detail: POC via `WorkOrderContacts` | Sem `siteCode` na listagem; sem POC na lista |
|
||||
| **WO** | `woNumber`, `rescheduleCount`, `carriedOver` | `internalWONumber` / `workerOrderNumber` | Formato ≠ 11 dígitos; sem badges ↻ ↷N |
|
||||
| **TYPE OF WO** | `type` (PM / Reactive / Emergency / Add-On / Overdue) | Não exposto (`WorkOrderType` em `db.txt`, não mapeado em EF) | Coluna inoperante |
|
||||
| **ASSIGNED TO** | `dispatcherId` + avatar/cor | `assignedTo` (nome completo); `AssignTo` = GUID Identity | Sem id/initials/color para avatar |
|
||||
| **SCHEDULE ON** | `scheduledOn`, `targetWeek` | Detail: `scheduledDate`; **list: ausente** | Listagem sem data de agendamento |
|
||||
| **DUE DATE** | `dueDate` | `dueDate` | **OK** na listagem |
|
||||
| **SERVICE** | `pm` | Detail: `trade` / `problem`; list: ausente | Coluna vazia na lista |
|
||||
| **VENDOR** | `company`, `tech`, `techPhone` | Detail: `dispatches` → vendor; list: ausente | Coluna vazia na lista |
|
||||
| **APPT TIME** | `apptTime` (ex: "07:00 – 09:00") | Entidade: `ScheduledStart`; sem `ScheduledEnd` | Janela de horário incompleta |
|
||||
| **STATUS** | 10 labels + overlay Past Due | String legada DB + enum 5 valores API | Sem mapping para status operacionais do FE |
|
||||
| **COMP DOC** | `docStatus` (Yes / No / NN) | Ausente no WO | Completion existe só no **dispatch** (`VerifyDispatch`) |
|
||||
| Actions | View / Edit | `GetWorkorderById` separado | Detalhe existe; contrato diferente |
|
||||
|
||||
---
|
||||
|
||||
## 3. Filtros e busca
|
||||
|
||||
### Barra principal (FE L5083-5099)
|
||||
|
||||
| Frontend | API hoje | Gap |
|
||||
|----------|----------|-----|
|
||||
| **Dispatcher** multi-select + `__unassigned__` + default usuário logado | `assignee` single string; `__unassigned` suportado em `GetWorkOrderList` | Multi-select e default "My WOs" não cobertos |
|
||||
| **Semana ativa** | Sem filtro `ScheduledDate` | Filtro semanal inexistente |
|
||||
| **Tipo** (All / PM / Reactive / Emergency / Add-On / Overdue) | Sem filtro por work order type | Inexistente |
|
||||
| **Busca contextual** na semana (site, wo#, dispatcher, location, pm, company, tech, status) | Search em 5 campos (`InternalWONumber`, `WorkerOrderNumber`, `Title`, `Location`), escopo global paginado | Campos e escopo incompatíveis |
|
||||
|
||||
### Advanced Search (FE L4255-4324)
|
||||
|
||||
| Frontend | API hoje | Gap |
|
||||
|----------|----------|-----|
|
||||
| Lista plana cross-week | Ausente | Sem endpoint dedicado |
|
||||
| Date range (this-week, last-week, this-month, last-3-months, next-week, next-month, custom) | `GetFilteredWorkorder` com due date buckets legados | Semântica diferente |
|
||||
| Filtros: sites[], types[], dispatchers[], statuses[], pmTypes[], vendorTechs[], docs[] | Parcial em endpoints legados | Cobertura incompleta |
|
||||
|
||||
---
|
||||
|
||||
## 4. Edição inline (spreadsheet-style)
|
||||
|
||||
| Frontend | API hoje | Gap |
|
||||
|----------|----------|-----|
|
||||
| PATCH por campo (site, wo#, type, dispatcher, schedule, due, service, vendor, appt, status…) | `EditWorkorder` (form multipart) + `ChangeStatus` + `ChangeAssignment` separados | Sem update granular por campo |
|
||||
| Auto-schedule: `Incomplete` → `Scheduled` quando data + dispatcher | Ausente no BE | Regra `maybeAutoSchedule` só no FE |
|
||||
| `rescheduleCount++` ao mudar `scheduledOn` | Campo inexistente | Badge ↻ impossível |
|
||||
| Limpar `isPastDue` ao reagendar para data futura | Campo derivado inexistente | Past Due não limpa via API |
|
||||
| Bloquear mudança de status se `isPastDue` | `ChangeStatus` aceita qualquer string | Sem validação 422 |
|
||||
| WO# único 11 dígitos normalizado | Gerador `WO-{yyyyMMdd}-{random}` ou sync sequencial `10000001` | Formato e unicidade incompatíveis |
|
||||
| WO cancelado read-only | Sem enforcement no BE | Só lógica no FE (`WOSlideOver` L2380) |
|
||||
|
||||
**Evidência BE:** `WorkOrderController.Editworkorder` L182-192 mapeia subset para `UpdateWorkOrderDTO` — ignora `Problem`, `Trade`, `ScheduledDate`, `Source` apesar de existirem em `EditWorkorder_DTO`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Criação de WO
|
||||
|
||||
| Frontend | API hoje | Gap |
|
||||
|----------|----------|-----|
|
||||
| Wizard `NewWOWizard` + inline `InlineRow` | `POST AddWorkorder` | Campos do wizard majoritariamente ausentes |
|
||||
| Campos: site, type, scheduledOn, targetWeek, pm, company, tech, appt, POC | `CreateWorkOrderDTO`: title, description, assignTo, dueDate, location | Contrato incompleto |
|
||||
| Status inicial `Incomplete` | Cria com `WorkOrderStatus.Open` | Status inicial diferente |
|
||||
| `scheduleWeekOnly` / `targetWeek` | Ausente | Week-only scheduling impossível |
|
||||
| Navegação automática para semana do WO criado | Depende de listagem semanal | Listagem incompatível |
|
||||
|
||||
---
|
||||
|
||||
## 6. Cancelamento
|
||||
|
||||
| Frontend | API hoje | Gap |
|
||||
|----------|----------|-----|
|
||||
| Soft cancel → status `Canceled` | `POST ChangeStatus(id, "Canceled")` manual | Sem endpoint `POST cancel` dedicado |
|
||||
| WO cancelado não editável | Sem bloqueio no BE | Enforcement só no FE |
|
||||
| Delete permanente (Admin) | `DELETE DeleteWorkorder` — delete físico | Comportamento diverge do soft cancel FE |
|
||||
|
||||
---
|
||||
|
||||
## 7. Slide-over (`WOSlideOver`)
|
||||
|
||||
| Tab FE | API hoje | Gap |
|
||||
|--------|----------|-----|
|
||||
| **Info** | `GET GetWorkorderById` | Omite `siteCode`; sem campos derivados FE (`isPastDue`, `rescheduleCount`, etc.) |
|
||||
| **Comments** | `AddComment` + `GetCommentsByWorkorderId` | Formato FE `{authorId, text, time}` vs BE `{Commenttext, FirstName, Documents}` |
|
||||
| **Audit Log** | `WorkOrderAuditLog` retornado no detalhe | Schema `{fieldName, oldValue, newValue, action}` vs FE `{type: manual\|system, dispatcherId?, action, time}` |
|
||||
| **Completion Doc** | `SignOffName` / `SignOffAttachment` no WO; `VerifyDispatch` no dispatch | Sem template por service type, PDF, `docStatus` WO-level |
|
||||
| **Extra Docs / Media** | `BeforPhoto`, `AfterPhoto`, `workOrderAttachments` | Sem `MediaFile.category`; sem API extra docs dedicada |
|
||||
|
||||
---
|
||||
|
||||
## 8. Regras de sistema (background jobs)
|
||||
|
||||
| Frontend assume | API hoje | Gap |
|
||||
|-----------------|----------|-----|
|
||||
| Job Past Due: `scheduledOn < hoje` + não terminal → `isPastDue=true` | Ausente (zero `BackgroundService` / Hangfire / Quartz no repo) | Flag nunca setada automaticamente |
|
||||
| Job Carried Over: virada de semana → `carriedOver++` | Ausente | Contador ↷N nunca incrementado |
|
||||
| Type Overdue promotion | Ausente | Filtro/tipo Overdue não automático |
|
||||
| Sync APM | `SyncController` POST manual (DynamoDB → SQL) | On-demand, não integrado à UX do board |
|
||||
|
||||
---
|
||||
|
||||
## 9. Lookups (catálogos)
|
||||
|
||||
| FE usa (mock estático) | API relacionada | Gap |
|
||||
|------------------------|-----------------|-----|
|
||||
| `DISPATCHERS` (id, name, initials, color) | `Common/GetUsers` | Sem initials/color; paginado; ids são GUIDs |
|
||||
| `SITE_OPTIONS` (code → city, state) | `Location/GetLocationList` | Sem lookup por site code; sem mapeamento BK5 → Dallas |
|
||||
| `PM_TYPES` | Ausente | Sem catálogo de service types |
|
||||
| `TECHNICIANS` (name, company, phone) | `Vendor/GetVendorList` | Sem endpoint technicians |
|
||||
| `WEEK_RANGES` (Mon–Fri + flag LIVE) | Ausente | Sem endpoint weeks |
|
||||
|
||||
---
|
||||
|
||||
## Contrato de dados — campo a campo
|
||||
|
||||
| Campo FE (`WorkOrder`) | Campo BE | Status |
|
||||
|------------------------|----------|--------|
|
||||
| `id` | `WorkOrder.Id` | SUPPORTED |
|
||||
| `woNumber` | `InternalWONumber` / `WorkerOrderNumber` | PARTIALLY_SUPPORTED — formatos conflitantes |
|
||||
| `site` | `SiteCode` | PARTIALLY_SUPPORTED — entidade tem; API omite na listagem |
|
||||
| `type` | — (`WorkOrderType` só em SQL) | NOT_SUPPORTED |
|
||||
| `dispatcherId` | `AssignTo` (GUID) | PARTIALLY_SUPPORTED |
|
||||
| `scheduledOn` | `ScheduledDate` | PARTIALLY_SUPPORTED — list omite |
|
||||
| `dayGroup` / `dayLabel` | — | NOT_SUPPORTED (derivado) |
|
||||
| `location` | `Locations.Name` | SUPPORTED (via join) |
|
||||
| `pm` | `Trade` / `Problem` | PARTIALLY_SUPPORTED |
|
||||
| `company` / `tech` | `Dispatches` → `Vendor` | PARTIALLY_SUPPORTED — não inline na lista |
|
||||
| `techPhone` | — | NOT_SUPPORTED |
|
||||
| `apptTime` | `ScheduledStart` (sem end) | PARTIALLY_SUPPORTED |
|
||||
| `status` | `Status` (string livre) | PARTIALLY_SUPPORTED — 10 vs 5+ formatos |
|
||||
| `docStatus` | — | NOT_SUPPORTED |
|
||||
| `pocName` / `pocPhone` / `pocNotes` | `WorkOrderContacts` | PARTIALLY_SUPPORTED — notes ausente |
|
||||
| `dueDate` | `DueDate` | SUPPORTED |
|
||||
| `rescheduleCount` | — | NOT_SUPPORTED |
|
||||
| `carriedOver` | — | NOT_SUPPORTED |
|
||||
| `originalWeek` / `originalDate` | — | NOT_SUPPORTED |
|
||||
| `isPastDue` | — | NOT_SUPPORTED (derivado) |
|
||||
| `targetWeek` | — | NOT_SUPPORTED |
|
||||
|
||||
---
|
||||
|
||||
## Regras de negócio — FE vs BE
|
||||
|
||||
| Regra FE | Evidência FE | BE hoje | Status |
|
||||
|----------|--------------|---------|--------|
|
||||
| Auto-schedule Incomplete → Scheduled | `maybeAutoSchedule` L500-506 | Ausente | NOT_SUPPORTED |
|
||||
| Reschedule incrementa contador | `updateScheduledOn` L4856 | Ausente | NOT_SUPPORTED |
|
||||
| Reschedule limpa Past Due | L4851-4857 | Ausente | NOT_SUPPORTED |
|
||||
| Status bloqueado em Past Due | `StatusCell` L1120-1159 | `ChangeStatus` sem validação | NOT_SUPPORTED |
|
||||
| WO# único 11 dígitos | `findDuplicateWONumber` L4897 | Gerador random; `InternalNumberExistsAsync` sem normalização | NOT_SUPPORTED |
|
||||
| Cancel → Canceled, read-only | `cancelWO` L4946; `WOSlideOver` L2380 | `ChangeStatus` manual | PARTIALLY_SUPPORTED |
|
||||
| Carried over job semanal | Audit derivado L2414-2416 | Sem job | NOT_SUPPORTED |
|
||||
| Completion doc por tipo de serviço | `CompDocDialog` L3902+ | Dispatch verify only | NOT_SUPPORTED (WO-level) |
|
||||
| Emergency/Reactive → media flow | `isMediaWO` L332-334 | Mídia WO genérica | PARTIALLY_SUPPORTED |
|
||||
|
||||
---
|
||||
|
||||
## Diagrama — contrato atual vs esperado
|
||||
|
||||
```
|
||||
HOJE (GetWorkOrderList) FRONTEND (WorkOrders.tsx)
|
||||
───────────────────── ─────────────────────────
|
||||
Paginação 12/page 1 call = semana inteira
|
||||
Sem ScheduledDate Agrupamento Mon–Fri
|
||||
Sem siteCode, type, vendor 14 colunas preenchidas
|
||||
Sem unscheduled semantics Seção Unscheduled fixa
|
||||
5 status legados 10 status + Past Due overlay
|
||||
CRUD admin Schedule Board operacional
|
||||
EditWorkorder (form) PATCH inline por célula
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cobertura por área
|
||||
|
||||
| Área | Cobertura estimada |
|
||||
|------|-------------------|
|
||||
| Listagem board (visão semanal) | ~5% |
|
||||
| Colunas da tabela (14) | ~15% |
|
||||
| Filtros e busca | ~10% |
|
||||
| Edição inline | ~0% |
|
||||
| Criação (wizard/inline) | ~25% |
|
||||
| Slide-over (detalhe) | ~40% |
|
||||
| Completion doc WO-level | ~0% |
|
||||
| Background jobs | ~0% |
|
||||
| Lookups/catálogos | ~20% |
|
||||
|
||||
---
|
||||
|
||||
## Critical blockers
|
||||
|
||||
1. **Sem API window-based** para visão semanal + Unscheduled — tela principal não carrega.
|
||||
2. **Contrato de listagem incompatível** com 14 colunas do board.
|
||||
3. **Campos persistidos ausentes** — `rescheduleCount`, `carriedOver`, `targetWeek`, `docStatus`, `isPastDue`.
|
||||
4. **Sem PATCH inline** — edição spreadsheet-style impossível.
|
||||
5. **Advanced search cross-week inexistente.**
|
||||
6. **Status 10 valores + Past Due** sem mapping layer implementado.
|
||||
7. **Completion doc WO-level inexistente** — coluna COMP DOC inoperante.
|
||||
8. **WO# 11 dígitos único** não suportado.
|
||||
9. **Jobs Past Due / Carried Over ausentes.**
|
||||
|
||||
---
|
||||
|
||||
## Non-critical gaps
|
||||
|
||||
- Reorder intra-dia persistido (FE já local-only)
|
||||
- Flags pessoais (FE session-only)
|
||||
- Paginação 12/24/48/96 (FE não implementou)
|
||||
- Bulk select (FE não implementou)
|
||||
- `[Authorize]` comentado no `WorkOrderController` (risco de segurança)
|
||||
|
||||
---
|
||||
|
||||
## Referências
|
||||
|
||||
- Frontend: `seahaven.desing/src/pages/WorkOrders.tsx`
|
||||
- Controller: `Api.SeaHavenIndustries/Controllers/WorkOrderController.cs`
|
||||
- Data service: `SeaHaven.DataServices/Implementation/WorkOrderDataService.cs`
|
||||
- Entidade: `Data.SeaHavenIndustries/Models/WorkerOrder.cs`
|
||||
- ADR (proposto, não implementado): `docs/adr/work-orders-board-api.md`
|
||||
- Spikes: `docs/spikes/consumer-audit-getworkorderlist.md`, `search-strategy.md`, `status-mapping.md`, `work-order-type.md`
|
||||
|
||||
---
|
||||
|
||||
## Classificação final
|
||||
|
||||
| Métrica | Valor |
|
||||
|---------|-------|
|
||||
| Compatibility Level | **Minimally Compatible** |
|
||||
| Backend Readiness | **Not Ready** |
|
||||
| Confidence | **High** |
|
||||
|
||||
Este documento reporta apenas **findings** — gaps entre o que a API entrega hoje e o que o frontend especifica. Não inclui propostas de implementação.
|
||||
|
|
@ -1,907 +0,0 @@
|
|||
# Roadmap de Implementação — Work Orders API vs Frontend Board
|
||||
|
||||
**Documento:** Relatório consolidado de arquitetura e entrega
|
||||
**Fonte da verdade (frontend):** `seahaven.desing/src/pages/WorkOrders.tsx`
|
||||
**Backend:** `seaheven.api`
|
||||
**Auditoria base:** [auditoria-work-orders-api-vs-frontend.md](auditoria-work-orders-api-vs-frontend.md)
|
||||
**Estado atual:** Compatibility Score **24/100** — backend **Not Ready**
|
||||
**Nota técnica do programa:** **9.0/10** (meta kickoff enterprise **9.5+**)
|
||||
**Escopo:** capacidades de negócio, domínio, governança, fases, riscos e validação — sem design de API, schema ou código
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
### 1.1 Readiness geral
|
||||
|
||||
O backend é **maduro para CRUD legado, dispatch, vendor portal, checklist/sign-off e audit**, mas foi construído para **listagem paginada administrativa**, não para o **Schedule Board operacional** que o frontend define. A tela principal do board **não pode funcionar** com os contratos atuais.
|
||||
|
||||
| Dimensão | Avaliação |
|
||||
|----------|-----------|
|
||||
| Tela principal (visão semanal) | ~5% |
|
||||
| 14 colunas do board | ~15% |
|
||||
| Edição inline | ~0% |
|
||||
| Completion doc WO-level | ~0% |
|
||||
| Background jobs | ~0% |
|
||||
| Detalhe / slide-over | ~40% |
|
||||
| **Média ponderada** | **~24%** |
|
||||
|
||||
### 1.2 Problemas arquiteturais principais
|
||||
|
||||
1. **Paradigma de contrato incompatível** — paginação global vs. janela semanal com seção Unscheduled fixa.
|
||||
2. **Projeção de listagem insuficiente** — 9 das 14 colunas do board não são alimentadas pela listagem atual.
|
||||
3. **Ausência de camada de domínio operacional** — campos derivados, tipo de WO e status operacionais ausentes ou não expostos.
|
||||
4. **Database drift** — `WorkOrderType` existe no SQL legado mas não no EF; possíveis outras colunas desalinhadas.
|
||||
5. **Modelo de mutação inadequado** — form multipart vs. edição granular por célula.
|
||||
6. **Sem automação de sistema** — zero infraestrutura de jobs; regras do FE nunca executam no servidor.
|
||||
7. **Cinco consumidores paralelos** — SHOC, Blazor EF, Vendor Portal, Sync/Lambda, e-mails — sem governança de ownership.
|
||||
8. **Duas trilhas paralelas** — Blazor acessa EF direto; API REST é integração do SHOC.
|
||||
|
||||
### 1.3 Maior risco do programa
|
||||
|
||||
**Governança de ownership de dados** — quem pode alterar cada campo entre SHOC, Vendor Portal, Sync, Blazor e Jobs. A arquitetura de domínio resolve fontes da verdade; a governança operacional é o gate final.
|
||||
|
||||
### 1.4 Decisões de produto registradas
|
||||
|
||||
- **Novas funcionalidades do board** → somente via API REST para o frontend React (SHOC).
|
||||
- **Blazor (`SeaHavenIndustries`)** → não recebe board, inline edit, jobs, advanced search nem completion doc WO-level; manutenção mínima até sunset do módulo WO.
|
||||
- **Vendor Portal** → permanece ativo; não-regressão obrigatória.
|
||||
- **DynamoDB/Sync** → ponte temporária; aposentar após SHOC ingerir direto ([TODO.md](../TODO.md)).
|
||||
|
||||
### 1.5 Complexidade e esforço
|
||||
|
||||
| Métrica | Valor |
|
||||
|---------|-------|
|
||||
| Complexidade geral | **Alta** |
|
||||
| Esforço total | **XL** (6–9 meses, 2 squads; ou 4–6 meses, 3 squads) |
|
||||
| Fase 0 — Fundação domínio + governança | L |
|
||||
| Fase 1 — Board semanal | L |
|
||||
| Fase 2 — Edição inline + concorrência | L |
|
||||
| Fase 3 — Criação e cancelamento | M |
|
||||
| Fase 4 — Busca avançada | M |
|
||||
| Fase 5 — Domain events (jobs) | M |
|
||||
| Fase 6 — Completion doc e slide-over | M |
|
||||
| Fase 7 — Rollout e produção | M |
|
||||
|
||||
### 1.6 Gates bloqueantes da Fase 0
|
||||
|
||||
Nenhuma implementação de board inicia sem:
|
||||
|
||||
1. Domain Architecture Review (DAR) aprovado
|
||||
2. **Data Ownership Model** assinado
|
||||
3. **Field Ownership Matrix** assinada
|
||||
4. **Audit Event Contract** aprovado
|
||||
5. Volume Discovery Report (sem premissa de volume)
|
||||
6. Database drift report (SQL vs EF)
|
||||
7. Spike vendor/dispatch
|
||||
8. Data migration dry-run
|
||||
9. RowVersion multi-agregado definido
|
||||
|
||||
---
|
||||
|
||||
## 2. Capability Gap Assessment
|
||||
|
||||
### 2.1 Weekly Scheduling Board — **Critical**
|
||||
- **Atual:** paginação sem janela temporal, sem Unscheduled.
|
||||
- **Desejado:** 1 call/semana, Mon–Fri + Unscheduled, contador `X of Y`.
|
||||
|
||||
### 2.2 Work Order Lifecycle — **High**
|
||||
- **Atual:** form parcial, status `Open`, delete físico.
|
||||
- **Desejado:** wizard/inline completo, `Incomplete`, soft cancel read-only.
|
||||
|
||||
### 2.3 Inline Editing — **Critical**
|
||||
- **Atual:** `EditWorkorder` multipart; endpoints separados.
|
||||
- **Desejado:** update granular por célula com regras embutidas.
|
||||
|
||||
### 2.4 Search & Filtering — **High**
|
||||
- **Atual:** 5 campos, escopo global, assignee single-select.
|
||||
- **Desejado:** busca contextual na semana + advanced search cross-week.
|
||||
|
||||
### 2.5 Status Management — **Critical**
|
||||
- **Atual:** enum 5 valores + string livre; sem Past Due.
|
||||
- **Desejado:** 10 labels + flag Past Due separada; auto-schedule; bloqueios.
|
||||
|
||||
### 2.6 Completion Documentation — **High**
|
||||
- **Atual:** sign-off básico; verify só no dispatch.
|
||||
- **Desejado:** `docStatus` WO-level (Yes/No/NN); templates por serviço.
|
||||
|
||||
### 2.7 Vendor Management (board) — **High**
|
||||
- **Atual:** vendor só no detalhe via dispatch.
|
||||
- **Desejado:** colunas VENDOR/APPT na lista; edição via dispatch primário.
|
||||
|
||||
### 2.8 Scheduling Intelligence — **Critical**
|
||||
- **Atual:** `ScheduledDate` no detalhe; sem `targetWeek`, `ScheduledEnd`.
|
||||
- **Desejado:** agendamento semântico completo; badges ↻ e ↷N.
|
||||
|
||||
### 2.9 Audit & Tracking — **Medium**
|
||||
- **Atual:** audit field-based legado.
|
||||
- **Desejado:** `{type: manual|system, dispatcherId?, action, time}` + eventos de sistema.
|
||||
|
||||
### 2.10 Background Automation — **Critical** (como otimização, não fonte da verdade)
|
||||
- **Atual:** zero jobs.
|
||||
- **Desejado:** WeekRolled, cache opcional PastDue; domínio recalcula on-read.
|
||||
|
||||
### 2.11 Lookup Data — **High**
|
||||
- **Atual:** lookups paginados incompatíveis; sem PM types, technicians.
|
||||
- **Desejado:** dispatchers (initials/color), sites por code, PM types, technicians.
|
||||
|
||||
### 2.12 Reporting / Contadores — **Medium**
|
||||
- **Atual:** `totalCount` global.
|
||||
- **Desejado:** `X of Y` semanal.
|
||||
|
||||
### 2.13 Security — **High**
|
||||
- **Atual:** `[Authorize]` comentado no `WorkOrderController`.
|
||||
- **Desejado:** auth ativa; default "My WOs".
|
||||
|
||||
---
|
||||
|
||||
## 3. Dependency Map
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph phase0 [Fase0_Fundacao]
|
||||
DAR[DAR e 3 artefatos]
|
||||
DOM[Agregados dominio]
|
||||
VOL[Volume Discovery]
|
||||
OWN[Data Ownership]
|
||||
end
|
||||
subgraph phase1 [Fase1_Board]
|
||||
B1[Listagem semanal]
|
||||
B2[14 colunas]
|
||||
end
|
||||
subgraph phase2 [Fase2_Mutacao]
|
||||
M1[Inline edit]
|
||||
M2[Concorrencia]
|
||||
end
|
||||
subgraph phase3 [Fase3_Lifecycle]
|
||||
L1[Criacao wizard]
|
||||
L2[Cancelamento]
|
||||
end
|
||||
subgraph phase45 [Fase4_5]
|
||||
S1[Search]
|
||||
E1[Domain events]
|
||||
end
|
||||
subgraph phase6 [Fase6_Completion]
|
||||
C1[DocStatus slideover]
|
||||
end
|
||||
DAR --> DOM
|
||||
DOM --> B1
|
||||
OWN --> M1
|
||||
VOL --> B1
|
||||
B1 --> B2 --> M1
|
||||
M1 --> L1
|
||||
B2 --> S1
|
||||
DOM --> E1
|
||||
B2 --> C1
|
||||
```
|
||||
|
||||
**Ordem obrigatória:** Fase 0 → Board → Inline → Lifecycle → Search/Events → Completion → Rollout.
|
||||
|
||||
---
|
||||
|
||||
## 4. Domain Model
|
||||
|
||||
### 4.1 Agregados e limites (anti God Entity)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph root [Aggregate Root WorkOrder]
|
||||
CORE[Core Lifecycle Assignment]
|
||||
SCH[Scheduling slice]
|
||||
TRK[Tracking slice]
|
||||
COMP[Completion slice]
|
||||
ANA[Analytics slice]
|
||||
end
|
||||
subgraph separate [Agregado separado]
|
||||
DISP[Dispatch fonte vendor]
|
||||
end
|
||||
AUD[WorkOrderAuditLog append-only]
|
||||
CORE --> SCH
|
||||
CORE --> TRK
|
||||
CORE --> ANA
|
||||
CORE --> COMP
|
||||
CORE --> DISP
|
||||
CORE --> AUD
|
||||
```
|
||||
|
||||
| Agregado | Responsabilidade |
|
||||
|----------|------------------|
|
||||
| **WorkOrder (core)** | Id, WoNumber, Type, LifecycleStatus, SiteCode, LocationId, DueDate, AssignTo, Title, RowVersion |
|
||||
| **Scheduling slice** | ScheduledDate, ScheduledStart/End, TargetWeek, ScheduleWeekOnly |
|
||||
| **Tracking slice** | OriginalWeek, OriginalDate (set-once) |
|
||||
| **Analytics slice** | RescheduleCount, CarriedOver (contadores históricos) |
|
||||
| **Completion slice** | DocStatus, refs attachments |
|
||||
| **Dispatch** | Vendor, tech, status portal — **fonte da verdade vendor** |
|
||||
| **Audit** | Rastreabilidade imutável |
|
||||
|
||||
**Proibido:** `VendorId`, `TechName`, `TechPhone` canônicos na WO.
|
||||
|
||||
### 4.2 Scheduling Aggregate Growth Watchlist
|
||||
|
||||
Campos que **não** entram em Scheduling sem ARB review: `MoveReason`, `MoveUser`, `MoveCategory`, `MoveSource`. Metadados de movimentação → Audit. Gate: slice Scheduling > 8 campos operacionais → ARB obrigatório.
|
||||
|
||||
### 4.3 Campos reaproveitados (sem nova coluna)
|
||||
|
||||
`SiteCode`, `ScheduledDate`, `DueDate`, `AssignTo`, `Trade`/`Problem`, `LocationId`, POC via `WorkOrderContacts`.
|
||||
|
||||
### 4.4 Database drift conhecido
|
||||
|
||||
| Coluna SQL | Ação |
|
||||
|------------|------|
|
||||
| `WorkOrderType` | Mapear no Core |
|
||||
| `AvettaTask` | Inventariar; mapear ou deprecar |
|
||||
| `AssignDate` | Inventariar; mapear ou deprecar |
|
||||
|
||||
Gate Fase 0: diff completo SQL vs `ApplicationDbContextModelSnapshot`.
|
||||
|
||||
### 4.5 Entidades relacionadas
|
||||
|
||||
| Entidade | Mudança | Fase |
|
||||
|----------|---------|------|
|
||||
| `WorkOrderContacts` | `Notes` (pocNotes) | 0 |
|
||||
| `WorkOrderAuditLog` | EventType Manual/System/Vendor/Sync | 0 |
|
||||
| `ApplicationUser` | Initials, Color | 0–1 |
|
||||
| `Locations` | SiteCode | 0–1 |
|
||||
| `WorkOrderAttachments` | Category | 2–6 |
|
||||
| `WorkOrderEnums` | LifecycleStatus, WorkOrderType, DocStatus, OperationalFlags | 0 |
|
||||
| `Dispatch` | Proteção regression | contínuo |
|
||||
|
||||
Fora de escopo imediato: `FollowUps`, `Quotes`, `PMSchedules`, `Employee`, `WorkOrderCategories` no board.
|
||||
|
||||
---
|
||||
|
||||
## 5. Domain Architecture Review (DAR)
|
||||
|
||||
### 5.1 Fonte da verdade por conceito
|
||||
|
||||
| Conceito | Fonte da verdade | Proibido |
|
||||
|----------|------------------|----------|
|
||||
| Status | `LifecycleStatus` (10 valores) | String livre; status composto |
|
||||
| Past Due | `OperationalFlags.PastDue` | Misturar no enum lifecycle |
|
||||
| Vendor/Tech | Dispatch primário | Duplicar na WO |
|
||||
| Schedule | Scheduling slice | Duplicar no core |
|
||||
| Completion | Completion.DocStatus | Só inferir de dispatch |
|
||||
| Reschedule/Carried | Analytics + audit | Job blind overwrite |
|
||||
| WO# | Campo canônico único | Dois números sem regra |
|
||||
| POC | WorkOrderContacts | Só no detalhe |
|
||||
|
||||
### 5.2 Matriz Persistido vs Derivado
|
||||
|
||||
| Campo FE | Persistido | Derivado | Fonte / Regra |
|
||||
|----------|------------|----------|---------------|
|
||||
| `lifecycleStatus` | Sim | Não | Core |
|
||||
| `isPastDue` | Não | **Sim** | `ScheduledDate < hoje` AND NOT terminal; job = cache opcional |
|
||||
| `type` Overdue | Não | **Sim** | Regra type + schedule + status |
|
||||
| `carriedOver` | Sim | Não | Métrica histórica; incremento via `WeekRolled` |
|
||||
| `rescheduleCount` | Sim | Parcial | Domain event em mudança de `ScheduledDate` |
|
||||
| `docStatus` | Sim | Não | Completion slice |
|
||||
| `dayGroup`, `apptTime` | Não | Sim | Calculados na leitura |
|
||||
| vendor/tech | Via Dispatch | Projeção | Join dispatch primário |
|
||||
|
||||
**Regra de ouro:** Jobs nunca são a única fonte da verdade. Leitura sempre recalcula derivados.
|
||||
|
||||
### 5.3 Lifecycle Status vs Operational Flags
|
||||
|
||||
```text
|
||||
LifecycleStatus → enum único (10 valores FE)
|
||||
OperationalFlags → PastDue, (futuro: Escalated)
|
||||
```
|
||||
|
||||
Filtro status = LifecycleStatus. Overlay Past Due = flag. Bloqueio de edição = validação sobre flag.
|
||||
|
||||
### 5.4 CarriedOver — justificativa
|
||||
|
||||
| | `isPastDue` | `carriedOver` |
|
||||
|--|-------------|---------------|
|
||||
| Natureza | Estado pontual | **Métrica histórica acumulada** |
|
||||
| Persistir | Não (derivado) | **Sim** (contador) |
|
||||
| Por quê | Calculável da data atual | Não recuperável só do schedule atual |
|
||||
|
||||
Invariante: incremento **somente** via domain event `WeekRolled`.
|
||||
|
||||
### 5.5 Vendor / Dispatch — spike obrigatório (Fase 0)
|
||||
|
||||
1. WO mantém `PrimaryDispatchId`
|
||||
2. Colunas VENDOR = projeção do dispatch primário
|
||||
3. Edição inline vendor = mutação no Dispatch
|
||||
4. WO sem dispatch → edição cria dispatch primário
|
||||
|
||||
Entregável: ADR "Vendor Source of Truth". Critério: zero `Tech*` canônicos na WO.
|
||||
|
||||
### 5.6 Jobs — domínio primeiro
|
||||
|
||||
| Job | Papel |
|
||||
|-----|-------|
|
||||
| Past Due diário | Cache refresh opcional; on-read sempre correto |
|
||||
| Carried Over semanal | Dispara `WeekRolled` → incrementa contador + audit system |
|
||||
| Promoção Overdue | Derivado on-read |
|
||||
| Audit | Síncrono em toda mutação desde Fase 0 |
|
||||
|
||||
### 5.7 Database drift investigation
|
||||
|
||||
Diff SQL vs EF; classificar: mapear, deprecar, mover para agregado satélite. Tabelas: `workOrders`, `Comments`, `Locations`, `Dispatch`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Data Ownership Model
|
||||
|
||||
### 6.1 Ownership por ator
|
||||
|
||||
| Ator | Pode alterar | Não pode alterar |
|
||||
|------|--------------|------------------|
|
||||
| **SHOC** | Lifecycle, Schedule, Assignment, Completion, vendor via Dispatch, POC, WO# | Checklist/signoff vendor |
|
||||
| **Vendor Portal** | Dispatch status, checklist, signoff, comments | Schedule, lifecycle, assignment, DueDate |
|
||||
| **Sync/Lambda** | Campos ingest (Description, ExternalId, etc.) — só owner Sync | Campos SHOC-owned após 1ª edição manual |
|
||||
| **Blazor** | Campos legados até sunset | Campos novos board |
|
||||
| **Jobs** | Incremento CarriedOver; audit system | Lifecycle, Schedule, Dispatch |
|
||||
|
||||
### 6.2 Field Ownership Matrix
|
||||
|
||||
| Campo | Agregado | Owner escritor | Sync sobrescreve? | Conflito SHOC vs Sync |
|
||||
|-------|----------|----------------|-------------------|----------------------|
|
||||
| LifecycleStatus | Core | SHOC | Não após 1ª edição SHOC | SHOC vence |
|
||||
| AssignTo | Core | SHOC | Não após 1ª edição SHOC | SHOC vence |
|
||||
| ScheduledDate / janela | Scheduling | SHOC | Não | SHOC vence |
|
||||
| TargetWeek | Scheduling | SHOC | Não | SHOC vence |
|
||||
| DueDate | Core | SHOC + Sync | Sim se nunca editado SHOC | SHOC se ManualEditFlag |
|
||||
| Description | Core | SHOC + Sync | Merge / SHOC priority | SHOC se editado |
|
||||
| SiteCode / Location | Core | SHOC + Sync | Sim na criação; não após SHOC | SHOC após flag |
|
||||
| WorkOrderType | Core | SHOC | Sim na criação ingest | SHOC após flag |
|
||||
| WoNumber | Core | SHOC | Não | SHOC only |
|
||||
| Vendor / tech | Dispatch | SHOC + Vendor | Não | Domínios separados |
|
||||
| Checklist/signoff | Dispatch | Vendor | Não | Vendor vence |
|
||||
| DocStatus | Completion | SHOC | Não | SHOC only |
|
||||
| RescheduleCount / CarriedOver | Analytics | Domain events | Não | N/A |
|
||||
| ExternalWorkOrderId | Core | Sync | Sim (idempotente) | Sync only |
|
||||
|
||||
**ManualEditFlag:** primeira edição SHOC marca campo; Sync subsequente skip + audit `SyncRejected`.
|
||||
|
||||
### 6.3 Sync merge — cenários
|
||||
|
||||
| Cenário | Comportamento |
|
||||
|---------|---------------|
|
||||
| Sync DueDate + SHOC ScheduledDate | Merge — campos distintos |
|
||||
| Sync Status + SHOC Status | SHOC vence; sync skip |
|
||||
| Sync AssignTo + SHOC AssignTo | SHOC vence |
|
||||
| Sync Description + SHOC Description | SHOC vence se ManualEditFlag |
|
||||
| Sync cria WO novo | Full ingest |
|
||||
| Sync atualiza por ExternalWorkOrderId | Field-level merge por matriz |
|
||||
|
||||
---
|
||||
|
||||
## 7. Concurrency Strategy
|
||||
|
||||
### 7.1 Atores
|
||||
|
||||
Dispatcher (SHOC), Vendor (Portal), Sync/Lambda, Blazor (congelado), Jobs.
|
||||
|
||||
### 7.2 RowVersion multi-agregado
|
||||
|
||||
| Agregado | RowVersion | Quando muda |
|
||||
|----------|------------|-------------|
|
||||
| **WorkOrder (root)** | No Core | Mutação Core, Scheduling, Completion, Analytics na mesma transação |
|
||||
| **Dispatch** | Próprio | Mutação vendor/tech/checklist/status dispatch |
|
||||
| **Projeção board** | N/A | FE envia `workOrderVersion` + `dispatchVersion` |
|
||||
|
||||
**Propagação:**
|
||||
- SHOC edita ScheduledDate → bump WO.RowVersion
|
||||
- SHOC edita vendor → bump Dispatch.RowVersion
|
||||
- Vendor edita checklist → bump Dispatch.RowVersion only
|
||||
- Sync altera Description → bump WO se merge aplicado
|
||||
- DocStatus → bump WO (Completion na mesma transação)
|
||||
|
||||
Conflito → 409 com `currentState` para refresh.
|
||||
|
||||
### 7.3 Concurrency Ownership Rules
|
||||
|
||||
| Cenário | Resultado |
|
||||
|---------|-----------|
|
||||
| SHOC vs SHOC (mesmo campo) | 409; último RowVersion válido |
|
||||
| SHOC vs Sync (mesmo campo) | Vence owner do campo |
|
||||
| SHOC vs Sync (campos diferentes) | Merge |
|
||||
| Vendor vs Sync | Domínios separados |
|
||||
| SHOC vs Vendor | Merge se campos distintos; impossível mesmo conceito por design |
|
||||
| Job WeekRolled vs SHOC reschedule | Idempotência WO+semana |
|
||||
| Blazor vs SHOC | SHOC vence; Blazor congelado |
|
||||
|
||||
### 7.4 Testes
|
||||
|
||||
Fase 2: conflito paralelo (2 dispatchers, sync durante edição); dual RowVersion; audit count = fields changed.
|
||||
|
||||
---
|
||||
|
||||
## 8. Audit Event Contract
|
||||
|
||||
### 8.1 Princípios
|
||||
|
||||
- Toda mutação gera audit **síncrono** desde Fase 0
|
||||
- Edição inline = **1 evento por campo**
|
||||
- Audit para investigação operacional, não só compliance
|
||||
|
||||
### 8.2 Schema por evento
|
||||
|
||||
| Campo | Obrigatório |
|
||||
|-------|-------------|
|
||||
| WorkOrderId | Sim |
|
||||
| EventType | Sim: Manual / System / Sync / Vendor |
|
||||
| Action | Sim: FieldChanged, StatusChanged, WeekRolled, SyncRejected |
|
||||
| FieldName | Sim para inline |
|
||||
| OldValue, NewValue | Sim |
|
||||
| ActorId, ActorType | Sim para Manual |
|
||||
| Timestamp | Sim UTC |
|
||||
| CorrelationId | Recomendado |
|
||||
| DispatchId | Quando evento é no dispatch |
|
||||
|
||||
### 8.3 Mutação → eventos
|
||||
|
||||
| Mutação | Eventos |
|
||||
|---------|---------|
|
||||
| ChangeStatus | 1× StatusChanged |
|
||||
| Inline cell | 1× FieldChanged por campo |
|
||||
| Auto-schedule | StatusChanged + FieldChanged se aplicável |
|
||||
| Reschedule | FieldChanged + RescheduleCount + audit analytics |
|
||||
| WeekRolled | System WeekRolled + CarriedOver old/new |
|
||||
| Sync skip | Sync SyncRejected |
|
||||
| Vendor checklist | Vendor FieldChanged no Dispatch |
|
||||
|
||||
### 8.4 Contrato FE (Audit tab)
|
||||
|
||||
`{ type, dispatcherId?, action, fieldName?, oldValue, newValue, time }`
|
||||
|
||||
---
|
||||
|
||||
## 9. Data Migration Plan
|
||||
|
||||
### 9.1 Status
|
||||
|
||||
| Legado | LifecycleStatus alvo |
|
||||
|--------|------------------------|
|
||||
| Open | Incomplete |
|
||||
| InProgress | In Progress |
|
||||
| Completed | Complete |
|
||||
| Cancelled | Canceled |
|
||||
| OnHold | On Hold |
|
||||
| Desconhecido | Incomplete + NeedsReview |
|
||||
|
||||
Processo: dry-run staging; 100% classificados; `LegacyStatus` read-only 1 release; rollback documentado.
|
||||
|
||||
### 9.2 WO#
|
||||
|
||||
| Formato legado | Estratégia |
|
||||
|----------------|------------|
|
||||
| Sequencial sync `10000001` | Normalizar 11 dígitos |
|
||||
| `WO-{date}-{random}` | Coexistência; novos só 11 dígitos |
|
||||
| Duplicatas | Resolução manual pré-go-live |
|
||||
|
||||
### 9.3 WorkOrderType
|
||||
|
||||
Mapear SQL existente → enum; NULL → default PO; Overdue = derivado.
|
||||
|
||||
### 9.4 Rollback
|
||||
|
||||
Migrations reversíveis; feature flag SHOC; snapshot pré-migration.
|
||||
|
||||
---
|
||||
|
||||
## 10. Volume Discovery e Search Scalability
|
||||
|
||||
### 10.1 Volume Discovery (Fase 0 — gate Fase 1)
|
||||
|
||||
**Sem premissa de volume.** Métricas obrigatórias:
|
||||
|
||||
| Métrica | Gate |
|
||||
|---------|------|
|
||||
| Total WOs produção/staging | Obrigatório |
|
||||
| WOs/semana (p95) | Obrigatório |
|
||||
| WOs Unscheduled | Obrigatório |
|
||||
| Crescimento mensal | Desejável |
|
||||
|
||||
### 10.2 Tiers e impacto
|
||||
|
||||
| Tier | Total WOs | Listagem semanal | Advanced search |
|
||||
|------|-----------|------------------|-----------------|
|
||||
| **S** | < 25k | Índices simples | SQL filtros compostos |
|
||||
| **M** | 25k–250k | Índices covering | Paginação obrigatória |
|
||||
| **L** | 250k–1M | Read model candidato | Full-text |
|
||||
| **XL** | > 1M | Materialized view | Search dedicado (ARB) |
|
||||
|
||||
Fases 1–3: índices conservadores compatíveis com qualquer tier. Fase 4: implementação conforme tier + load test.
|
||||
|
||||
---
|
||||
|
||||
## 11. Coexistência Multi-Consumidor
|
||||
|
||||
### 11.1 Inventário
|
||||
|
||||
| Consumidor | Conexão | Status |
|
||||
|------------|---------|--------|
|
||||
| **SHOC React** (`seahaven.desing`) | REST JWT | Em desenvolvimento — **único alvo novas features** |
|
||||
| **Blazor** (`SeaHavenIndustries`) | EF direto, não REST | Ativo — manutenção mínima; sunset WO |
|
||||
| **Vendor Portal UI** | REST `X-Vendor-Token` | API ativa; UI externa |
|
||||
| **SyncController + Lambda** | DynamoDB → SQL | Ponte temporária |
|
||||
| **E-mails SendGrid** | Deep links SHOC/Portal | Ativo |
|
||||
| **Mobile** | — | Sem evidência — confirmar com PO |
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
SHOC[SHOC React] -->|REST JWT| API[Api REST]
|
||||
VPortal[Vendor Portal] -->|X-Vendor-Token| API
|
||||
Lambda[Lambda ingest] --> DynamoDB[(DynamoDB)]
|
||||
DynamoDB --> Sync[SyncController]
|
||||
Sync --> DB[(SQL Server)]
|
||||
API --> DB
|
||||
Blazor[Blazor EF] -->|legado| DB
|
||||
```
|
||||
|
||||
**Nota:** DynamoDB é **ingestão externa**, não banco operacional. SQL Server é fonte para board, Blazor e Portal.
|
||||
|
||||
### 11.2 Matriz de coexistência
|
||||
|
||||
| Consumidor | Novas features board | Schema | Sunset |
|
||||
|------------|---------------------|--------|--------|
|
||||
| SHOC | Recebe tudo | Consome novos campos | Destino final |
|
||||
| Blazor | Não recebe | Colunas nullable; backward compatible | Módulo WO congela |
|
||||
| Vendor Portal | Não usa board | Dispatch intacto | Mantém |
|
||||
| Sync/Lambda | Indireto | Defaults para novos campos | Após cutover API |
|
||||
| E-mails | Links SHOC | FrontendBaseUrl prod | Atualizar templates |
|
||||
|
||||
### 11.3 Trilhas de entrega
|
||||
|
||||
- **Trilha A SHOC:** fases 0–7; feature flags; piloto dispatchers
|
||||
- **Trilha B Blazor:** smoke EF pós-migration; sem board; bugfix crítico only
|
||||
- **Trilha C Vendor Portal:** regression suite cada release que toca WO/Dispatch
|
||||
- **Trilha D Lambda:** manter até SHOC produção; cutover Fase 7; critério: criação WO + auth serviço estáveis
|
||||
|
||||
### 11.4 Endpoints legado vs board
|
||||
|
||||
- Legado (`GetWorkOrderList`, etc.): manter durante coexistência; congelar contrato
|
||||
- Board: novos contratos; não estender legado com hacks
|
||||
- Deprecação: após 100% dispatchers SHOC + sunset Blazor WO
|
||||
|
||||
### 11.5 Ordem de rollout
|
||||
|
||||
1. Fase 0 staging — gates + migrations
|
||||
2. Gate Blazor smoke EF
|
||||
3. Fase 1 staging — SHOC board feature flag
|
||||
4. Gate Vendor Portal regression
|
||||
5. Fases 2–6 incrementais
|
||||
6. Piloto 1–2 dispatchers
|
||||
7. Rollout gradual
|
||||
8. Sunset Blazor WO
|
||||
9. Cutover Lambda → API
|
||||
10. Deprecação legado + SyncController
|
||||
|
||||
### 11.6 Monitoramento coexistência
|
||||
|
||||
Métricas por consumidor; drift DynamoDB vs SQL; adoção SHOC vs Blazor; alertas job failure.
|
||||
|
||||
### 11.7 Checklist pré-go-live PO
|
||||
|
||||
- App mobile externo?
|
||||
- Data sunset Blazor WO
|
||||
- FrontendBaseUrl produção → SHOC
|
||||
- Owner Lambda cutover
|
||||
|
||||
---
|
||||
|
||||
## 12. Implementation Phases
|
||||
|
||||
### Fase 0 — Domain Foundation + Governança
|
||||
|
||||
**Objetivo:** Kickoff enterprise — domínio, ownership, volume, audit antes de board.
|
||||
|
||||
**Escopo:** DAR; 3 artefatos (Ownership, Field Matrix, Audit Contract); Volume Discovery; drift SQL/EF; spike vendor; agregados Core+Scheduling+Tracking+Analytics+Completion; enums; RowVersion root+Dispatch; migration dry-run; ManualEditFlag design; audit baseline ChangeStatus/ChangeAssignment; smoke Blazor/Sync/Portal.
|
||||
|
||||
**Não inicia:** Board API, inline edit, jobs.
|
||||
|
||||
**Validação:** 9 gates assinados; audit granularidade testada; tier volume definido.
|
||||
|
||||
---
|
||||
|
||||
### Fase 1 — Weekly Board (leitura)
|
||||
|
||||
**Objetivo:** Tela principal carrega semana operacional.
|
||||
|
||||
**Escopo:** Listagem window-based; Unscheduled; 14 colunas como projeção; `X of Y`; filtros (dispatcher multi, `__unassigned__`, My WOs, tipo, semana); `isPastDue` derivado on-read; vendor via dispatch primário; índices conforme tier volume.
|
||||
|
||||
**Validação:** 1 call = semana + Unscheduled; 12/14 colunas mínimo; gate Vendor Portal regression.
|
||||
|
||||
---
|
||||
|
||||
### Fase 2 — Inline Edit + Concorrência
|
||||
|
||||
**Objetivo:** Edição spreadsheet com regras e conflitos tratados.
|
||||
|
||||
**Escopo:** Update granular; dual RowVersion; audit 1 evento/campo; auto-schedule; rescheduleCount++; bloqueio PastDue flag; cancel read-only; WO# 11 dígitos; testes concorrência.
|
||||
|
||||
---
|
||||
|
||||
### Fase 3 — Criação e Cancelamento
|
||||
|
||||
**Escopo:** Wizard/inline completo; Incomplete inicial; targetWeek; soft cancel; ManualEditFlag na criação; delete admin documentado.
|
||||
|
||||
---
|
||||
|
||||
### Fase 4 — Search
|
||||
|
||||
**Escopo:** Busca contextual semana; advanced search cross-week conforme tier; load test; sem implementar advanced sem tier definido.
|
||||
|
||||
---
|
||||
|
||||
### Fase 5 — Scheduled Domain Events
|
||||
|
||||
**Escopo:** WeekRolled → CarriedOver++; cache opcional PastDue; idempotência; monitoramento falha; on-read sempre correto.
|
||||
|
||||
---
|
||||
|
||||
### Fase 6 — Completion Doc e Slide-over
|
||||
|
||||
**Escopo:** docStatus WO-level; templates por serviço; Comments/Audit/Media alinhados ao FE.
|
||||
|
||||
---
|
||||
|
||||
### Fase 7 — Rollout e Produção
|
||||
|
||||
**Escopo:** Piloto; rollout gradual; sunset Blazor WO; cutover Lambda; deprecação legado; UAT; zero P1 duas semanas; dashboards saúde.
|
||||
|
||||
---
|
||||
|
||||
## 13. Critical Path Analysis
|
||||
|
||||
### Must Have
|
||||
1. Listagem window-based + Unscheduled
|
||||
2. Projeção 14 colunas
|
||||
3. Agregados domínio + enums
|
||||
4. Update granular por campo
|
||||
5. Status 10 labels + flag PastDue
|
||||
6. Lookups
|
||||
7. Criação wizard
|
||||
8. WO# 11 dígitos
|
||||
9. DAR + 3 artefatos Fase 0
|
||||
|
||||
### Should Have
|
||||
10. Domain events CarriedOver
|
||||
11. Advanced search
|
||||
12. Regras mutação completas
|
||||
13. Soft cancel enforcement
|
||||
14. docStatus WO-level
|
||||
15. Auth ativa
|
||||
|
||||
### Nice to Have
|
||||
16. Reorder intra-dia persistido (FE local)
|
||||
17. Flags pessoais session-only
|
||||
18. Paginação 12/24/48/96
|
||||
19. Bulk select
|
||||
|
||||
---
|
||||
|
||||
## 14. Frontend Impact Assessment
|
||||
|
||||
| Gap | UX | Severidade | Workaround |
|
||||
|-----|-----|------------|------------|
|
||||
| Sem listagem semanal | Tela inutilizável | Critical | Mock FE |
|
||||
| 14 colunas incompletas | Board ilegível | Critical | Nenhum em prod |
|
||||
| Sem inline edit | Core inoperante | Critical | Modal legado |
|
||||
| Sem advanced search | WOs históricos invisíveis | High | Busca por ID |
|
||||
| Status incompatíveis | Decisões erradas | Critical | Hardcode FE |
|
||||
| Sem jobs (derivados) | Badges stale se só client | High | On-read BE resolve PastDue |
|
||||
| Sem docStatus | COMP DOC vazia | High | Ocultar coluna |
|
||||
| WO# formato errado | Criação bloqueada | Critical | — |
|
||||
| Ownership Sync vs SHOC | Overwrite silencioso | Critical | Field Matrix Fase 0 |
|
||||
| Auth desabilitada | Risco segurança | High | API gateway |
|
||||
|
||||
---
|
||||
|
||||
## 15. Business Rules Assessment
|
||||
|
||||
| Regra | Status | Impacto |
|
||||
|-------|--------|---------|
|
||||
| Auto-schedule Incomplete→Scheduled | Unsupported | Status incorreto |
|
||||
| Reschedule → rescheduleCount++ | Unsupported | Badge ↻ ausente |
|
||||
| Reschedule limpa PastDue (derivado) | Unsupported | Flag stale até re-read |
|
||||
| Status bloqueado se PastDue | Unsupported | Bypass via API |
|
||||
| WO# único 11 dígitos | Unsupported | Duplicatas |
|
||||
| Cancel read-only | Partial | Edição pós-cancel |
|
||||
| WeekRolled → carriedOver | Unsupported | Badge ↷N ausente |
|
||||
| Completion doc por serviço | Unsupported | Workflow manual |
|
||||
| Week-only scheduling | Unsupported | Wizard incompleto |
|
||||
|
||||
---
|
||||
|
||||
## 16. Data Contract Readiness
|
||||
|
||||
| Área | Readiness % | Gap principal |
|
||||
|------|-------------|---------------|
|
||||
| List Views board | 5% | 14 colunas, schedule, vendor, flags |
|
||||
| Detail slide-over | 40% | Derivados, docStatus, schema |
|
||||
| Search | 10% | Cross-week, date ranges |
|
||||
| Lookups | 20% | initials, site code, PM, tech |
|
||||
| Scheduling | 25% | targetWeek, ScheduledEnd |
|
||||
| Status | 30% | 10 labels + flag |
|
||||
| Completion | 0% | docStatus, templates |
|
||||
| Comments | 60% | Schema |
|
||||
| Audit | 50% | type system, granularidade |
|
||||
| Media | 45% | category |
|
||||
|
||||
**Média ponderada: ~28%**
|
||||
|
||||
---
|
||||
|
||||
## 17. Testing Strategy
|
||||
|
||||
### Functional
|
||||
Board semanal, 14 colunas, filtros, inline por campo, regras negócio, criação/cancelamento, docStatus, status+flag, ownership rejection sync.
|
||||
|
||||
### Integration
|
||||
SHOC E2E por fase; Vendor Portal regression; Sync; auth; dual RowVersion.
|
||||
|
||||
### Regression
|
||||
Endpoints legados; Blazor smoke EF; Portal checklist/signoff.
|
||||
|
||||
### UAT
|
||||
Roteiros dispatchers reais; sign-off PO por fase.
|
||||
|
||||
### Data validation
|
||||
WO# migration; status mapping; derivados on-read vs contadores; referencial.
|
||||
|
||||
### Migration validation
|
||||
Dump produção espelho; antes/depois contagens; rollback por fase.
|
||||
|
||||
### Fase-specific
|
||||
- **0:** volume queries; audit granularidade; sync skip
|
||||
- **2:** concurrency suite; 409 handling
|
||||
- **4:** load test tier
|
||||
- **5:** job failure injection; on-read correctness
|
||||
|
||||
---
|
||||
|
||||
## 18. Risk Matrix
|
||||
|
||||
| Risco | Prob. | Impacto | Mitigação |
|
||||
|-------|-------|---------|-----------|
|
||||
| Board inoperante em prod | Alta | Crítico | Fase 1 gate; E2E |
|
||||
| Ownership ambíguo Sync vs SHOC | Alta | Crítico | Field Ownership Matrix |
|
||||
| God Entity creep | Média | Alto | Growth Watchlist; ARB |
|
||||
| Conflito cross-agregado | Média | Alto | RowVersion root + Dispatch |
|
||||
| Job falha estado errado | Alta | Alto | Derivados on-read |
|
||||
| Vendor drift Dispatch vs WO | Alta | Crítico | Spike; projeção only |
|
||||
| Status migration incorreta | Média | Crítico | LegacyStatus; dry-run |
|
||||
| Volume desconhecido → índices errados | Alta | Alto | Volume Discovery Fase 0 |
|
||||
| Audit inútil investigação | Média | Médio | Event Contract |
|
||||
| Regressão Vendor Portal | Média | Crítico | Suite dedicada |
|
||||
| Blazor vs SHOC divergência | Alta | Alto | Congelar Blazor WO |
|
||||
| Auth bloqueia integrações | Média | Alto | Inventário consumidores |
|
||||
| CarriedOver recalculado errado | Baixa | Médio | Só via WeekRolled |
|
||||
| Scheduling aggregate creep | Média | Alto | Watchlist §4.2 |
|
||||
|
||||
---
|
||||
|
||||
## 19. Prioritized Backlog
|
||||
|
||||
Ordenado por sequência de implementação.
|
||||
|
||||
| # | Item | P | Fase | Dep | Complex. | Risco |
|
||||
|---|------|---|------|-----|----------|-------|
|
||||
| 1 | DAR completo + sign-off CTO | P0 | 0 | — | M | Alto |
|
||||
| 2 | Data Ownership Model | P0 | 0 | — | S | Alto |
|
||||
| 3 | Field Ownership Matrix + ManualEditFlag | P0 | 0 | 2 | M | Alto |
|
||||
| 4 | Audit Event Contract + baseline | P0 | 0 | — | M | Médio |
|
||||
| 5 | Volume Discovery Report | P0 | 0 | — | S | Alto |
|
||||
| 6 | Database drift SQL vs EF | P0 | 0 | — | M | Médio |
|
||||
| 7 | Spike vendor/dispatch source of truth | P0 | 0 | — | M | Alto |
|
||||
| 8 | Agregados domínio (anti God Entity) | P0 | 0 | 1 | L | Alto |
|
||||
| 9 | Matriz Persistido vs Derivado | P0 | 0 | 1 | S | Médio |
|
||||
| 10 | RowVersion root + Dispatch | P0 | 0 | 8 | M | Alto |
|
||||
| 11 | Enums Lifecycle + Flags + WorkOrderType | P0 | 0 | 8 | M | Médio |
|
||||
| 12 | Data migration dry-run | P0 | 0 | 8 | M | Alto |
|
||||
| 13 | Inventário consumidores + coexistência | P0 | 0 | — | S | Médio |
|
||||
| 14 | Smoke Blazor EF + Sync + Portal | P0 | 0 | 8 | M | Alto |
|
||||
| 15 | Normalização WO# 11 dígitos | P0 | 0 | 12 | M | Alto |
|
||||
| 16 | Reativar auth endpoints WO | P0 | 0 | — | S | Médio |
|
||||
| 17 | Lookups (dispatchers, sites, PM, tech) | P0 | 0–1 | — | M | Baixo |
|
||||
| 18 | Listagem window-based + Unscheduled | P0 | 1 | 8,11,17 | L | Alto |
|
||||
| 19 | Projeção 14 colunas | P0 | 1 | 18 | L | Alto |
|
||||
| 20 | isPastDue derivado on-read | P0 | 1 | 19 | S | Baixo |
|
||||
| 21 | Vendor via dispatch primário | P0 | 1 | 7,19 | M | Alto |
|
||||
| 22 | Filtros principais + My WOs | P0 | 1 | 18 | M | Médio |
|
||||
| 23 | Contador X of Y | P1 | 1 | 18 | S | Baixo |
|
||||
| 24 | Sync merge por field ownership | P0 | 0–1 | 3 | M | Alto |
|
||||
| 25 | Update granular inline | P0 | 2 | 19 | L | Alto |
|
||||
| 26 | Audit 1 evento por campo | P0 | 2 | 4,25 | M | Médio |
|
||||
| 27 | Concurrency integration tests | P0 | 2 | 10,25 | M | Alto |
|
||||
| 28 | Auto-schedule Incomplete→Scheduled | P0 | 2 | 25 | M | Médio |
|
||||
| 29 | RescheduleCount++ domain event | P0 | 2 | 25 | S | Baixo |
|
||||
| 30 | Bloqueio status se PastDue flag | P0 | 2 | 25 | S | Baixo |
|
||||
| 31 | Cancel read-only enforcement | P1 | 2 | 25 | S | Baixo |
|
||||
| 32 | Criação wizard/inline completa | P0 | 3 | 25 | M | Médio |
|
||||
| 33 | Week-only scheduling targetWeek | P1 | 3 | 32 | M | Médio |
|
||||
| 34 | Soft cancel dedicado | P1 | 3 | 31 | S | Baixo |
|
||||
| 35 | Busca contextual semana | P1 | 4 | 19 | M | Baixo |
|
||||
| 36 | Advanced search cross-week | P1 | 4 | 5,19 | L | Médio |
|
||||
| 37 | Load test search por tier | P0 | 4 | 5 | M | Alto |
|
||||
| 38 | WeekRolled domain event + CarriedOver | P1 | 5 | 8 | M | Médio |
|
||||
| 39 | Cache opcional PastDue | P2 | 5 | 20 | S | Baixo |
|
||||
| 40 | docStatus WO-level | P1 | 6 | 8 | M | Médio |
|
||||
| 41 | Completion templates por serviço | P2 | 6 | 40 | L | Médio |
|
||||
| 42 | Slide-over Comments/Audit/Media | P2 | 6 | 4,40 | M | Baixo |
|
||||
| 43 | Regression suite legado + Portal | P0 | cont. | — | M | Alto |
|
||||
| 44 | UAT dispatchers por fase | P0 | cont. | — | S | Médio |
|
||||
| 45 | Piloto + rollout gradual SHOC | P0 | 7 | 1–42 | M | Alto |
|
||||
| 46 | Sunset Blazor WO | P1 | 7 | 45 | S | Médio |
|
||||
| 47 | Cutover Lambda → API | P1 | 7 | 32 | M | Alto |
|
||||
| 48 | Deprecação endpoints legado + Sync | P2 | 7 | 45 | M | Médio |
|
||||
| 49 | Monitoramento coexistência | P1 | 7 | 45 | S | Baixo |
|
||||
|
||||
---
|
||||
|
||||
## 20. Cronograma indicativo
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title Work Orders Board
|
||||
dateFormat YYYY-MM-DD
|
||||
section Fundacao
|
||||
DAR_arteFatos_dominio :2026-07-01, 8w
|
||||
section Board
|
||||
Listagem_14colunas :2026-09-01, 8w
|
||||
section Mutacao
|
||||
Inline_concorrencia :2026-11-01, 8w
|
||||
Criacao_cancelamento :2026-11-15, 6w
|
||||
section Inteligencia
|
||||
Search :2027-01-01, 6w
|
||||
Domain_events :2027-01-15, 4w
|
||||
section Completion
|
||||
DocStatus_slideover :2027-02-15, 6w
|
||||
section Producao
|
||||
Piloto_rollout :2027-04-01, 8w
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 21. Stakeholders
|
||||
|
||||
| Papel | Responsabilidade |
|
||||
|-------|------------------|
|
||||
| CEO / Sponsor | Investimento XL; prioridade SHOC sobre Blazor |
|
||||
| Product Owner | Aceite por fase; semana operacional; regras ambíguas |
|
||||
| CTO / Arquiteto | DAR; 3 artefatos; ownership; cutover Lambda |
|
||||
| Backend | Fases 0–6 domínio e contratos |
|
||||
| Frontend SHOC | Integração; feature flags; remover mocks |
|
||||
| QA | Testes §17; UAT; concurrency |
|
||||
| Documentação | Contratos; guia consumidores legados |
|
||||
| Operações | Jobs; alertas; monitoramento coexistência |
|
||||
|
||||
---
|
||||
|
||||
## 22. Critérios de prontidão para kickoff (9.5+)
|
||||
|
||||
| Área | Critério |
|
||||
|------|----------|
|
||||
| Domain Design | DAR aprovado; agregados; Watchlist |
|
||||
| Source of Truth | Field Ownership Matrix assinada |
|
||||
| Data Governance | Data Ownership Model completo |
|
||||
| Concurrency | RowVersion root + Dispatch; testes Fase 2 |
|
||||
| Sync | Cenários explícitos; ManualEditFlag |
|
||||
| Audit | Event Contract; 1 evento/campo inline |
|
||||
| Migration | Dry-run; LegacyStatus rollback |
|
||||
| Volume | Discovery Fase 0; tier definido |
|
||||
| Jobs | Domain events; on-read correto se job falha |
|
||||
| Coexistência | Inventário; trilhas; smoke Blazor/Portal/Sync |
|
||||
|
||||
---
|
||||
|
||||
## 23. Conclusão
|
||||
|
||||
Programa **viável e de grande porte (XL)**. O caminho crítico é:
|
||||
|
||||
```text
|
||||
Resolver domínio e governança (Fase 0)
|
||||
→ Board semanal (Fase 1)
|
||||
→ Inline edit + concorrência (Fase 2)
|
||||
→ Paridade funcional progressiva (Fases 3–6)
|
||||
→ Rollout seguro (Fase 7)
|
||||
```
|
||||
|
||||
O documento integra: gap assessment da auditoria, modelo de domínio com agregados, governança de ownership, concorrência multi-agregado, migração de dados, coexistência SHOC/Blazor/Portal/Lambda, e backlog executável — pronto para Architecture Review Board e kickoff de implementação enterprise.
|
||||
|
||||
**Próximo passo imediato:** executar Fase 0 — produzir e assinar os três artefatos (Data Ownership Model, Field Ownership Matrix, Audit Event Contract) antes de qualquer migration de schema.
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
# Spike Search Strategy — `GetWorkOrdersBoard`
|
||||
|
||||
**Data:** 2026-06-19
|
||||
**Gate:** G3 (pré-requisito Fase 1)
|
||||
**Objetivo:** Definir busca mínima viável para o Schedule Board sem degradar performance.
|
||||
|
||||
---
|
||||
|
||||
## Estado atual (`GetWorkOrderList`)
|
||||
|
||||
Implementação em `SeaHaven.DataServices/Implementation/WorkOrderDataService.cs` (`GetWorkOrderListPagedAsync`):
|
||||
|
||||
| Aspecto | Comportamento atual |
|
||||
|---------|---------------------|
|
||||
| Paginação | `page` / `pageSize` (default 12), sem cap |
|
||||
| Janela temporal | **Ausente** — sem filtro `ScheduledDate` |
|
||||
| Search | `Contains` + `ToLower()` em 5 campos |
|
||||
| Campos search | `InternalWONumber`, `WorkerOrderNumber`, `WorkerOrderTitle`, `Locations.Title`, `Locations.Name` |
|
||||
| Campos omitidos | `SiteCode`, `Trade`, `Problem`, vendor, dispatcher por ID |
|
||||
| Joins | `Locations`, `AssignToUser` (AspNetUsers) |
|
||||
| Vendor/dispatch | **Não incluídos** |
|
||||
| Projeção extra | Subquery `lastUpdated` via Comments + WorkOrderAuditLogs por linha |
|
||||
|
||||
### Padrão SQL gerado (search)
|
||||
|
||||
```sql
|
||||
WHERE LOWER(InternalWONumber) LIKE '%term%'
|
||||
OR LOWER(WorkerOrderNumber) LIKE '%term%'
|
||||
OR LOWER(WorkerOrderTitle) LIKE '%term%'
|
||||
OR LOWER(Locations.Name) LIKE '%term%'
|
||||
...
|
||||
```
|
||||
|
||||
Leading wildcard → table scan em colunas `nvarchar(max)`.
|
||||
|
||||
---
|
||||
|
||||
## Respostas às perguntas do spike
|
||||
|
||||
| # | Pergunta | Decisão Fase 1 |
|
||||
|---|----------|----------------|
|
||||
| S1 | Volume típico por semana | Assumir **< 200 WOs/semana** até medir em prod; threshold fallback = **300** |
|
||||
| S2 | Campos pesquisáveis mínimos | `internalWONumber`, `siteCode`, `assigneeId[]` (exato), `locationId` |
|
||||
| S3 | LIKE vs full-text | **Sem full-text** Fase 1; `StartsWith` em WO# e siteCode; evitar `Contains` em title/location no servidor |
|
||||
| S4 | Escopo | Search **sempre scoped** a `scheduledFrom` / `scheduledTo` (obrigatório no endpoint) |
|
||||
| S5 | Vendor/tech no search | **Fase 2** — exige join `Dispatches → Vendor` |
|
||||
|
||||
---
|
||||
|
||||
## Estratégia em 2 camadas
|
||||
|
||||
```
|
||||
Request GetWorkOrdersBoard
|
||||
│
|
||||
├─► 1. Filtro janela ScheduledDate (obrigatório)
|
||||
│
|
||||
├─► 2. Filtros exatos: status, assigneeId[], locationId
|
||||
│
|
||||
├─► 3. COUNT no escopo
|
||||
│
|
||||
├─► count <= 300?
|
||||
│ ├─ SIM → retornar semana inteira (sem paginação Fase 1)
|
||||
│ │ search textual extra no FE: title, locationLabel, serviceType
|
||||
│ └─ NÃO → exigir critério indexável no servidor
|
||||
│ (woNumber ou siteCode StartsWith)
|
||||
│
|
||||
└─► Response: { scheduledFrom, scheduledTo, totalCount, items[] }
|
||||
```
|
||||
|
||||
### Campos Fase 1 vs Fase 2
|
||||
|
||||
| Campo | Fase 1 (servidor) | Fase 1 (client fallback) | Fase 2 |
|
||||
|-------|-------------------|--------------------------|--------|
|
||||
| `internalWONumber` / `workerOrderNumber` | `StartsWith` | — | — |
|
||||
| `siteCode` | `StartsWith` ou igualdade | — | — |
|
||||
| `assigneeId` | multi-select exato | — | — |
|
||||
| `locationId` | exato | — | — |
|
||||
| `status` | exato / multi | pills client-side | — |
|
||||
| `workerOrderTitle` | — | client-side se count ≤ 300 | server `Contains` opcional |
|
||||
| `locationLabel` | — | client-side | — |
|
||||
| `serviceType` (trade/category) | — | client-side | server join |
|
||||
| `vendorCompany` / `vendorTechnician` | — | — | join Dispatch→Vendor |
|
||||
| `assigneeName` | — | client-side | evitar join por nome |
|
||||
|
||||
---
|
||||
|
||||
## Query plan esperado (Fase 1)
|
||||
|
||||
### Caminho feliz (semana típica, ≤ 300 rows)
|
||||
|
||||
1. **Index seek** em `IX_workOrders_ScheduledDate` (proposto) com range `[scheduledFrom, scheduledTo+1day)`
|
||||
2. Filtro `istemplate != true`
|
||||
3. Filtros exatos em `AssignTo`, `LocationId`, `Status` (AND)
|
||||
4. **Sem** search textual no servidor se `search` vazio
|
||||
5. Join LEFT `Locations`, `AssignToUser`, LEFT `Dispatches` + `Vendor` (Fase 1 board — vendor no DTO, não no search)
|
||||
6. Projeção flat para `WorkOrderBoardRowDto` — **sem** subquery `lastUpdated`
|
||||
|
||||
### Caminho search servidor (count > 300 ou user digitou termo indexável)
|
||||
|
||||
1. Mesma janela + filtros exatos
|
||||
2. AND (`InternalWONumber LIKE 'term%'` OR `SiteCode LIKE 'term%'`)
|
||||
3. Se count ainda > 300 → HTTP 400 com mensagem "Refine search or narrow date window"
|
||||
|
||||
### Anti-padrões proibidos no board endpoint
|
||||
|
||||
- `pageSize=500` como gambiarra de fetch semanal
|
||||
- `Contains('%x%')` em múltiplas tabelas sem janela temporal
|
||||
- Filtro assignee por nome concatenado (`FirstName + LastName`) — usar `assigneeId`
|
||||
- Subquery correlated Comments/AuditLog por row
|
||||
|
||||
---
|
||||
|
||||
## Índices recomendados
|
||||
|
||||
Migration separada **após aprovação** desta spike (não incluir na Fase 1 code sem review):
|
||||
|
||||
| Índice | Coluna(s) | Prioridade | Nota |
|
||||
|--------|-----------|------------|------|
|
||||
| `IX_workOrders_ScheduledDate` | `ScheduledDate` | **P1** | Filtro de janela do board |
|
||||
| `IX_workOrders_InternalWONumber` | `InternalWONumber` | P2 | Requer alterar coluna de `nvarchar(max)` → `nvarchar(50)` |
|
||||
| `IX_workOrders_SiteCode` | `SiteCode` | P2 | Idem — tamanho fixo ~50 |
|
||||
| `IX_workOrders_AssignTo` | `AssignTo` | existente | Multi-select dispatcher |
|
||||
| `IX_workOrders_LocationId` | `LocationId` | existente | Filtro location |
|
||||
|
||||
**Full-text index:** não recomendado Fase 1 — volume semanal baixo + fallback client-side suficiente.
|
||||
|
||||
---
|
||||
|
||||
## Referência de implementação
|
||||
|
||||
Copiar padrão de janela temporal de `DispatchController.List`:
|
||||
|
||||
```csharp
|
||||
if (dateFrom.HasValue)
|
||||
q = q.Where(d => d.DispatchedAt >= dateFrom.Value);
|
||||
if (dateTo.HasValue)
|
||||
{
|
||||
var end = dateTo.Value.Date.AddDays(1);
|
||||
q = q.Where(d => d.DispatchedAt < end);
|
||||
}
|
||||
```
|
||||
|
||||
Adaptar para `WorkOrder.ScheduledDate` no board endpoint.
|
||||
|
||||
---
|
||||
|
||||
## Parâmetros propostos — `GetWorkOrdersBoard`
|
||||
|
||||
| Parâmetro | Tipo | Obrigatório | Descrição |
|
||||
|-----------|------|-------------|-----------|
|
||||
| `scheduledFrom` | `DateOnly` | **Sim** | Início da semana/janela |
|
||||
| `scheduledTo` | `DateOnly` | **Sim** | Fim da janela (inclusivo) |
|
||||
| `search` | `string` | Não | WO# ou siteCode prefix |
|
||||
| `assignee` | `string[]` | Não | IDs de dispatcher (multi) |
|
||||
| `status` | `string[]` | Não | Status canônico API |
|
||||
| `locationId` | `int?` | Não | Filtro location |
|
||||
|
||||
**Limite hard janela:** max **90 dias** (Fase 2 advanced search cross-week).
|
||||
|
||||
---
|
||||
|
||||
## Threshold e fallback
|
||||
|
||||
| Constante | Valor | Justificativa |
|
||||
|-----------|-------|---------------|
|
||||
| `BoardSearchClientSideThreshold` | **300** | Payload ~300 rows × ~500 bytes ≈ 150 KB — aceitável para 1 call/semana |
|
||||
| Sem paginação Fase 1 | — | Janela semanal é o filtro natural |
|
||||
| Paginação Fase 2 | cursor/offset | Só quando janela > 1 semana com search global |
|
||||
|
||||
---
|
||||
|
||||
## Conclusão
|
||||
|
||||
Fase 1 adota **window-first, search-second**:
|
||||
|
||||
1. Janela `ScheduledDate` obrigatória + índice dedicado
|
||||
2. Search servidor mínimo (`StartsWith` em identificadores)
|
||||
3. Fallback client-side para campos ricos quando count ≤ 300
|
||||
4. Vendor/tech e full-text adiados para Fase 2
|
||||
|
||||
Esta estratégia desbloqueia `GetWorkOrdersBoard` sem replicar os anti-padrões de `GetWorkOrderListPagedAsync`.
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# Fase 0 — Work Orders Foundation
|
||||
|
||||
Documentação e gates da Fase 0 (Domain Foundation + Governança).
|
||||
|
||||
## Governança
|
||||
|
||||
- [DAR — Domain Architecture Review](dar-domain-architecture-review.md)
|
||||
- [Matriz Persistido vs Derivado](dar-persisted-vs-derived-matrix.md)
|
||||
- [Data Ownership Model](data-ownership-model.md)
|
||||
- [Field Ownership Matrix](field-ownership-matrix.md)
|
||||
- [ManualEditFlag Design](manual-edit-flag-design.md)
|
||||
- [Audit Event Contract](audit-event-contract.md)
|
||||
|
||||
## Discovery
|
||||
|
||||
- [Volume Discovery Report](volume-discovery-report.md)
|
||||
- [Database Drift Report](database-drift-report.md)
|
||||
- [Consumer Inventory](consumer-inventory.md)
|
||||
- [ADR Vendor Source of Truth](adr-vendor-source-of-truth.md)
|
||||
|
||||
## Validação
|
||||
|
||||
- [RowVersion Design](rowversion-design.md)
|
||||
- [Migration Dry-Run Report](migration-dry-run-report.md)
|
||||
- [Smoke Checklist](smoke-checklist.md)
|
||||
- [Gates Sign-Off](phase-0-gates-signoff.md)
|
||||
|
||||
## Código entregue
|
||||
|
||||
- Migration: `Data.SeaHavenIndustries/Migrations/20260624163145_Phase0_DomainFoundation.cs`
|
||||
- Serviços: `IWorkOrderAuditService`, `IWorkOrderFieldLockService`, `ISyncFieldMergePolicy`
|
||||
- Testes: `SeaHavenIndustries.Tests` (12 testes)
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
# ADR — Vendor Source of Truth
|
||||
|
||||
**Status:** Accepted (Fase 0 spike)
|
||||
**Decisores:** Backend + Arquiteto
|
||||
|
||||
---
|
||||
|
||||
## Contexto
|
||||
|
||||
O board SHOC exibe colunas VENDOR e APPT na listagem semanal. Hoje vendor existe apenas no agregado Dispatch; a entidade WorkOrder não possui referência ao dispatch primário.
|
||||
|
||||
---
|
||||
|
||||
## Decisão
|
||||
|
||||
1. **WorkOrder.PrimaryDispatchId** (nullable FK → Dispatches) identifica o dispatch canônico para projeção board.
|
||||
2. Colunas VENDOR/APPT = **projeção read-only** via join no dispatch primário (Vendor.Name, Dispatch.ScheduledDate).
|
||||
3. Edição inline vendor (Fase 2) muta **Dispatch**, não WorkOrder. Bump `Dispatch.RowVersion`.
|
||||
4. WO sem dispatch: primeira edição vendor cria dispatch primário e seta `PrimaryDispatchId`.
|
||||
5. **Proibido** adicionar VendorId, TechName, TechPhone na tabela workOrders.
|
||||
|
||||
---
|
||||
|
||||
## Query de projeção (spike)
|
||||
|
||||
```sql
|
||||
SELECT wo.Id,
|
||||
wo.InternalWONumber,
|
||||
d.Id AS DispatchId,
|
||||
v.Name AS VendorName,
|
||||
d.ScheduledDate AS ApptDate,
|
||||
d.Status AS DispatchStatus
|
||||
FROM workOrders wo
|
||||
LEFT JOIN Dispatches d ON d.Id = wo.PrimaryDispatchId
|
||||
LEFT JOIN Vendors v ON v.Id = d.VendorId
|
||||
WHERE wo.IsDeleted IS NULL OR wo.IsDeleted = 0;
|
||||
```
|
||||
|
||||
Implementação C# em `DispatchDataService.GetPrimaryDispatchProjectionAsync(workOrderId)` (Fase 1).
|
||||
|
||||
---
|
||||
|
||||
## Seleção do dispatch primário
|
||||
|
||||
| Cenário | Regra |
|
||||
|---------|-------|
|
||||
| PrimaryDispatchId setado | Usar esse dispatch |
|
||||
| Null + exatamente 1 dispatch | Auto-set PrimaryDispatchId na migration backfill |
|
||||
| Null + N dispatches | Usar dispatch mais recente por DispatchedAt; log para revisão manual |
|
||||
| Zero dispatches | Vendor columns vazias no board |
|
||||
|
||||
---
|
||||
|
||||
## Consequências
|
||||
|
||||
- **Positivo:** Zero drift vendor WO vs Dispatch; Portal regression isolada.
|
||||
- **Negativo:** Join extra na listagem board — mitigado por índice em PrimaryDispatchId.
|
||||
- **Fase 0:** Coluna + FK + backfill script no dry-run; endpoint board na Fase 1.
|
||||
|
||||
---
|
||||
|
||||
## Critérios de aceite spike
|
||||
|
||||
- [x] ADR documentado
|
||||
- [x] Query prototipada
|
||||
- [ ] PrimaryDispatchId na migration Phase0
|
||||
- [ ] Backfill documentado no dry-run report
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
# Audit Event Contract — Work Orders
|
||||
|
||||
**Fase:** 0
|
||||
**Status:** Aprovado para implementação baseline
|
||||
**Schema EF:** `WorkOrderAuditLog` estendido
|
||||
|
||||
---
|
||||
|
||||
## 1. Princípios
|
||||
|
||||
- Toda mutação gera audit **síncrono** desde Fase 0.
|
||||
- Edição inline (Fase 2) = **1 evento por campo**.
|
||||
- Audit serve investigação operacional, não só compliance.
|
||||
|
||||
---
|
||||
|
||||
## 2. Schema por evento
|
||||
|
||||
| Campo | Tipo | Obrigatório |
|
||||
|-------|------|-------------|
|
||||
| Id | int | Sim (PK) |
|
||||
| WorkOrderId | int | Sim |
|
||||
| EventType | enum string | Sim: Manual, System, Sync, Vendor |
|
||||
| Action | string | Sim: FieldChanged, StatusChanged, WeekRolled, SyncRejected, AssignmentChanged |
|
||||
| FieldName | string | Sim para inline / field change |
|
||||
| OldValue | string | Sim |
|
||||
| NewValue | string | Sim |
|
||||
| UserId / ActorId | string | Sim para Manual |
|
||||
| ActorType | enum string | Sim: Dispatcher, Vendor, System, Sync |
|
||||
| CreatedAt | DateTime UTC | Sim |
|
||||
| CorrelationId | string | Recomendado |
|
||||
| DispatchId | int? | Quando evento é no dispatch |
|
||||
|
||||
**Compatibilidade:** colunas `Action` legada mapeada para novo `Action`; `UserId` = ActorId para Manual.
|
||||
|
||||
---
|
||||
|
||||
## 3. Enums
|
||||
|
||||
```csharp
|
||||
AuditEventType: Manual | System | Sync | Vendor
|
||||
AuditActorType: Dispatcher | Vendor | System | Sync
|
||||
AuditActionType: FieldChanged | StatusChanged | AssignmentChanged | WeekRolled | SyncRejected | Create | Delete
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Mutação → eventos
|
||||
|
||||
| Mutação | Eventos |
|
||||
|---------|---------|
|
||||
| ChangeStatus | 1× StatusChanged (EventType=Manual) |
|
||||
| ChangeAssignment | 1× AssignmentChanged (FieldName=AssignTo) |
|
||||
| Inline cell (Fase 2) | 1× FieldChanged por campo |
|
||||
| Auto-schedule (Fase 2) | StatusChanged + FieldChanged se aplicável |
|
||||
| Reschedule (Fase 2) | FieldChanged + analytics |
|
||||
| WeekRolled (Fase 5) | System WeekRolled + CarriedOver |
|
||||
| Sync skip | Sync SyncRejected |
|
||||
| Vendor checklist | Vendor FieldChanged + DispatchId |
|
||||
|
||||
---
|
||||
|
||||
## 5. Contrato FE (Audit tab)
|
||||
|
||||
```typescript
|
||||
interface AuditEntry {
|
||||
type: 'manual' | 'system' | 'sync' | 'vendor';
|
||||
dispatcherId?: string;
|
||||
action: string;
|
||||
fieldName?: string;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
time: string; // ISO UTC
|
||||
dispatchId?: number;
|
||||
}
|
||||
```
|
||||
|
||||
Mapeamento API → FE:
|
||||
|
||||
| API EventType | FE type |
|
||||
|---------------|---------|
|
||||
| Manual | manual |
|
||||
| System | system |
|
||||
| Sync | sync |
|
||||
| Vendor | vendor |
|
||||
|
||||
---
|
||||
|
||||
## 6. Baseline Fase 0 (endpoints)
|
||||
|
||||
| Endpoint | Garantia |
|
||||
|----------|----------|
|
||||
| POST ChangeStatus | 1 audit StatusChanged; Old/New preenchidos |
|
||||
| POST ChangeAssignment | 1 audit AssignmentChanged; nomes usuário em Old/New |
|
||||
|
||||
Implementação via `IWorkOrderAuditService`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Critérios de aceite
|
||||
|
||||
- [ ] Migration estende WorkOrderAuditLog
|
||||
- [ ] ChangeStatus/ChangeAssignment usam serviço central
|
||||
- [ ] Testes assertam 1 evento por mutação
|
||||
|
||||
**Assinatura Backend Lead:** _________________ Data: _______
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
# Consumer Inventory — Work Orders
|
||||
|
||||
**Fase:** 0
|
||||
**Referência:** roadmap §11
|
||||
|
||||
---
|
||||
|
||||
## 1. Inventário de consumidores
|
||||
|
||||
| Consumidor | Conexão | Código principal | Campos mutados | Audit hoje? |
|
||||
|------------|---------|------------------|----------------|-------------|
|
||||
| **SHOC React** | REST JWT | `Api.SeaHavenIndustries/Controllers/WorkOrderController.cs` | Status, AssignTo, CRUD parcial via Service | Parcial (ChangeStatus/Assignment) |
|
||||
| **Blazor** | EF direto | `SeaHavenIndustries/Data/Services/WorkorderService.cs` | CRUD completo, comments, status | **Não** |
|
||||
| **Vendor Portal** | REST token | `Api.SeaHavenIndustries/Controllers/VendorPortalController.cs` | Dispatch checklist, signoff, status | Sim (ad hoc) |
|
||||
| **Sync/Lambda** | DynamoDB → SQL | `Api.SeaHavenIndustries/Controllers/SyncController.cs` | Upsert WO por ExternalWorkOrderId | **Não** |
|
||||
| **Dispatch API** | REST | `DispatchController.cs` | Dispatch create/update/verify | Sim |
|
||||
| **E-mail SendGrid** | Deep links | `Helper/SendMessage` | Nenhum (read-only links) | N/A |
|
||||
|
||||
---
|
||||
|
||||
## 2. Diagrama
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
SHOC[SHOC_React] -->|REST_JWT| API[Api_REST]
|
||||
VPortal[Vendor_Portal] -->|X_Vendor_Token| API
|
||||
Lambda[Lambda_ingest] --> DynamoDB[(DynamoDB)]
|
||||
DynamoDB --> Sync[SyncController]
|
||||
Sync --> DB[(SQL_Server)]
|
||||
API --> DB
|
||||
Blazor[Blazor_EF] -->|legado| DB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Trilhas de entrega (coexistência)
|
||||
|
||||
| Trilha | Escopo Fase 0+ |
|
||||
|--------|----------------|
|
||||
| **A — SHOC** | Fases 0–7; feature flags; piloto dispatchers |
|
||||
| **B — Blazor** | Smoke EF pós-migration; bugfix crítico only |
|
||||
| **C — Vendor Portal** | Regression checklist cada release WO/Dispatch |
|
||||
| **D — Lambda/Sync** | Manter até SHOC produção; merge policy Fase 0 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Endpoints legado vs board
|
||||
|
||||
| Tipo | Exemplos | Política |
|
||||
|------|----------|----------|
|
||||
| Legado | GetWorkOrderList, ChangeStatus | Congelar contrato |
|
||||
| Board (Fase 1+) | GetWorkOrdersBoard | Novos contratos separados |
|
||||
|
||||
---
|
||||
|
||||
## 5. Gaps Fase 0 endereçados
|
||||
|
||||
| Gap | Mitigação |
|
||||
|-----|-----------|
|
||||
| Blazor sem audit | Congelar; não expandir |
|
||||
| Sync overwrite | ISyncFieldMergePolicy + ManualEditFlag |
|
||||
| Auth WO desabilitada | Reativar `[Authorize]` WorkOrderController |
|
||||
| WO# inconsistente | Documentar normalização 11 dígitos (Fase 2 execução) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Checklist pré-go-live PO
|
||||
|
||||
- [ ] App mobile externo confirmado?
|
||||
- [ ] Data sunset Blazor WO definida?
|
||||
- [ ] FrontendBaseUrl produção → SHOC?
|
||||
- [ ] Owner Lambda cutover nomeado?
|
||||
|
||||
---
|
||||
|
||||
## 7. Auth — consumidores e tokens
|
||||
|
||||
| Consumidor | Auth |
|
||||
|------------|------|
|
||||
| SHOC | JWT Bearer |
|
||||
| Sync | JWT Bearer (`[Authorize]` no SyncController) |
|
||||
| Vendor Portal | X-Vendor-Token (rotas públicas dispatch) |
|
||||
| Blazor | Cookie Identity (app separado) |
|
||||
|
||||
Reativar auth em WorkOrderController exige SHOC enviar JWT em todas as chamadas WO.
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
# DAR — Domain Architecture Review (Work Orders)
|
||||
|
||||
**Fase:** 0
|
||||
**Status:** Draft para sign-off CTO
|
||||
**Referência:** [roadmap-work-orders-board.md](../../roadmap-work-orders-board.md) §4–5
|
||||
|
||||
---
|
||||
|
||||
## 1. Objetivo
|
||||
|
||||
Formalizar o modelo de domínio operacional do Schedule Board antes de qualquer migration de schema board ou contrato de listagem semanal.
|
||||
|
||||
---
|
||||
|
||||
## 2. Agregados e limites (anti God Entity)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph root [AggregateRoot_WorkOrder]
|
||||
CORE[Core_Lifecycle_Assignment]
|
||||
SCH[Scheduling_slice]
|
||||
TRK[Tracking_slice]
|
||||
COMP[Completion_slice]
|
||||
ANA[Analytics_slice]
|
||||
end
|
||||
subgraph separate [Agregado_separado]
|
||||
DISP[Dispatch_fonte_vendor]
|
||||
end
|
||||
AUD[WorkOrderAuditLog_append_only]
|
||||
CORE --> SCH
|
||||
CORE --> TRK
|
||||
CORE --> ANA
|
||||
CORE --> COMP
|
||||
CORE --> DISP
|
||||
CORE --> AUD
|
||||
```
|
||||
|
||||
| Agregado / Slice | Responsabilidade | Persistência Fase 0 |
|
||||
|------------------|------------------|---------------------|
|
||||
| **Core** | Id, WoNumber, Type, LifecycleStatus, SiteCode, LocationId, DueDate, AssignTo, Title, RowVersion, PrimaryDispatchId | Colunas em `workOrders` |
|
||||
| **Scheduling** | ScheduledDate, ScheduledStart/End, TargetWeek, ScheduleWeekOnly | Colunas em `workOrders` |
|
||||
| **Tracking** | OriginalWeek, OriginalDate (set-once) | Colunas em `workOrders` |
|
||||
| **Analytics** | RescheduleCount, CarriedOver | Colunas em `workOrders` |
|
||||
| **Completion** | DocStatus, refs attachments | Coluna DocStatus + attachments existentes |
|
||||
| **Dispatch** | Vendor, tech, status portal | Tabela `Dispatches` + RowVersion |
|
||||
| **Audit** | Rastreabilidade imutável | `WorkOrderAuditLogs` |
|
||||
|
||||
**Proibido:** `VendorId`, `TechName`, `TechPhone` canônicos na entidade WorkOrder. Vendor sempre via Dispatch primário.
|
||||
|
||||
---
|
||||
|
||||
## 3. Fonte da verdade por conceito
|
||||
|
||||
| Conceito | Fonte da verdade | Proibido |
|
||||
|----------|------------------|----------|
|
||||
| Status operacional | `LifecycleStatus` (10 valores FE) | String livre `Status` como canônico |
|
||||
| Past Due | Derivado on-read (`ScheduledDate < hoje` AND NOT terminal) | Misturar no enum lifecycle |
|
||||
| Vendor / Tech | Dispatch primário (`PrimaryDispatchId`) | Duplicar na WO |
|
||||
| Schedule | Scheduling slice | Duplicar no core sem slice lógico |
|
||||
| Completion | `DocStatus` | Inferir só de dispatch signoff |
|
||||
| Reschedule / CarriedOver | Analytics + domain events | Job blind overwrite |
|
||||
| WO# | `InternalWONumber` / `WorkerOrderNumber` com regra 11 dígitos (Fase 2+) | Dois números sem regra |
|
||||
| POC | `WorkOrderContacts` + Notes | Só no detalhe sem contato |
|
||||
|
||||
---
|
||||
|
||||
## 4. Lifecycle Status vs Operational Flags
|
||||
|
||||
```text
|
||||
LifecycleStatus → enum único (10 valores alinhados ao frontend)
|
||||
OperationalFlags → PastDue (derivado on-read; cache opcional Fase 5)
|
||||
LegacyStatus → string original read-only (migration Fase 0)
|
||||
```
|
||||
|
||||
Filtro de status no board = `LifecycleStatus`. Overlay Past Due = flag derivada. Bloqueio de edição = validação sobre flag (Fase 2).
|
||||
|
||||
### Mapeamento legado → LifecycleStatus
|
||||
|
||||
| Legado (`Status`) | LifecycleStatus |
|
||||
|-------------------|-----------------|
|
||||
| Open | Incomplete |
|
||||
| InProgress / In Progress | InProgress |
|
||||
| Completed / Complete | Complete |
|
||||
| Cancelled / Canceled | Canceled |
|
||||
| OnHold / On Hold | OnHold |
|
||||
| Desconhecido | Incomplete + NeedsReview |
|
||||
|
||||
---
|
||||
|
||||
## 5. CarriedOver — justificativa
|
||||
|
||||
| | isPastDue | carriedOver |
|
||||
|--|-----------|-------------|
|
||||
| Natureza | Estado pontual | Métrica histórica acumulada |
|
||||
| Persistir | Não (derivado) | Sim (contador) |
|
||||
| Incremento | N/A | Somente via domain event `WeekRolled` (Fase 5) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Scheduling Aggregate Growth Watchlist
|
||||
|
||||
Campos que **não** entram em Scheduling sem ARB review:
|
||||
|
||||
- `MoveReason`, `MoveUser`, `MoveCategory`, `MoveSource`
|
||||
|
||||
Metadados de movimentação → Audit Event Contract.
|
||||
|
||||
**Gate:** slice Scheduling > 8 campos operacionais → ARB obrigatório.
|
||||
Contagem Fase 0: ScheduledDate, ScheduledStart, ScheduledEnd, TargetWeek, ScheduleWeekOnly, OriginalWeek, OriginalDate (tracking separado) — dentro do limite.
|
||||
|
||||
---
|
||||
|
||||
## 7. Jobs — domínio primeiro
|
||||
|
||||
| Job | Papel |
|
||||
|-----|-------|
|
||||
| Past Due diário | Cache refresh opcional; on-read sempre correto |
|
||||
| Carried Over semanal | Dispara `WeekRolled` → incrementa contador + audit system |
|
||||
| Promoção Overdue type | Derivado on-read |
|
||||
| Audit | Síncrono em toda mutação desde Fase 0 |
|
||||
|
||||
**Regra de ouro:** Jobs nunca são a única fonte da verdade.
|
||||
|
||||
---
|
||||
|
||||
## 8. Critérios de aceite (sign-off CTO)
|
||||
|
||||
- [ ] Zero campos vendor canônicos na WO
|
||||
- [ ] Agregados documentados e refletidos no schema Fase 0
|
||||
- [ ] Persistido vs derivado validado com PO (ver `dar-persisted-vs-derived-matrix.md`)
|
||||
- [ ] Watchlist Scheduling registrada
|
||||
- [ ] Blazor congelado — sem novos campos board via EF direto
|
||||
|
||||
**Assinaturas**
|
||||
|
||||
| Papel | Nome | Data |
|
||||
|-------|------|------|
|
||||
| CTO / Arquiteto | | |
|
||||
| Product Owner | | |
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
# DAR — Matriz Persistido vs Derivado
|
||||
|
||||
**Fase:** 0
|
||||
**Status:** Draft para validação PO + CTO
|
||||
|
||||
---
|
||||
|
||||
## Matriz de campos (board + domínio)
|
||||
|
||||
| Campo FE / conceito | Persistido | Derivado | Fonte / Regra |
|
||||
|---------------------|------------|----------|---------------|
|
||||
| lifecycleStatus | Sim | Não | Core — `LifecycleStatus` enum |
|
||||
| isPastDue | Não | **Sim** | `ScheduledDate < UTC hoje` AND NOT status terminal |
|
||||
| type (Overdue) | Parcial | **Sim** | Regra type + schedule + status |
|
||||
| workOrderType | Sim | Não | Core — enum `WorkOrderType` |
|
||||
| carriedOver | Sim | Não | Analytics — incremento via `WeekRolled` only |
|
||||
| rescheduleCount | Sim | Parcial | Analytics — incremento em mudança ScheduledDate |
|
||||
| docStatus | Sim | Não | Completion slice |
|
||||
| dayGroup | Não | Sim | Calculado na leitura a partir de ScheduledDate |
|
||||
| apptTime | Não | Sim | Projeção Dispatch primário ScheduledDate/Start |
|
||||
| vendorName / tech | Não | Sim (projeção) | Join Dispatch primário |
|
||||
| targetWeek | Sim | Não | Scheduling slice |
|
||||
| scheduledEnd | Sim | Não | Scheduling slice |
|
||||
| originalWeek / originalDate | Sim | Não | Tracking — set-once na primeira agenda |
|
||||
| operationalFlags.pastDue | Não* | Sim | *Cache opcional Fase 5; on-read authoritative |
|
||||
| legacyStatus | Sim (read-only) | Não | Valor string original pré-migration |
|
||||
|
||||
---
|
||||
|
||||
## Regras de derivação (on-read)
|
||||
|
||||
### isPastDue
|
||||
|
||||
```text
|
||||
isPastDue = ScheduledDate.HasValue
|
||||
AND ScheduledDate.Value.Date < DateTime.UtcNow.Date
|
||||
AND LifecycleStatus NOT IN (Complete, Canceled)
|
||||
```
|
||||
|
||||
### type Overdue (derivado)
|
||||
|
||||
WO com type != Overdue pode exibir badge Overdue quando isPastDue && regras de negócio PO confirmadas.
|
||||
|
||||
---
|
||||
|
||||
## Validação PO
|
||||
|
||||
| Pergunta | Resposta PO |
|
||||
|----------|-------------|
|
||||
| Past Due separado do lifecycle status? | Sim |
|
||||
| carriedOver persiste mesmo após reschedule? | Sim |
|
||||
| Vendor nunca duplicado na WO? | Sim |
|
||||
|
||||
**Assinatura PO:** _________________ Data: _______
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
# Data Ownership Model — Work Orders
|
||||
|
||||
**Fase:** 0
|
||||
**Status:** Draft para sign-off CTO
|
||||
|
||||
---
|
||||
|
||||
## 1. Princípios
|
||||
|
||||
1. **SHOC (React)** é o único destino de novas features do board.
|
||||
2. **SQL Server** é fonte operacional para board, Blazor e Portal.
|
||||
3. **DynamoDB/Sync** é ponte temporária de ingestão externa.
|
||||
4. Conflito entre atores resolve-se pela Field Ownership Matrix, não por last-write-wins global.
|
||||
|
||||
---
|
||||
|
||||
## 2. Ownership por ator
|
||||
|
||||
| Ator | Conexão | Pode alterar | Não pode alterar |
|
||||
|------|---------|--------------|------------------|
|
||||
| **SHOC** | REST JWT (`api/WorkOrder`) | Lifecycle, Schedule, Assignment, Completion, vendor via Dispatch, POC, WO# | Checklist/signoff vendor |
|
||||
| **Vendor Portal** | REST `X-Vendor-Token` | Dispatch status, checklist, signoff, comments vendor | Schedule, lifecycle, assignment, DueDate |
|
||||
| **Sync/Lambda** | `POST api/Sync/WorkOrders` | Campos ingest (Description, ExternalId, SiteCode na criação, etc.) | Campos SHOC-owned após ManualEditFlag |
|
||||
| **Blazor** | EF direto (`WorkorderService`) | Campos legados existentes até sunset | Campos novos board; **congelado** |
|
||||
| **Jobs** | Domain events (Fase 5+) | Incremento CarriedOver; audit System | Lifecycle, Schedule, Dispatch |
|
||||
|
||||
---
|
||||
|
||||
## 3. Política Blazor (sunset)
|
||||
|
||||
- Manutenção mínima: bugfix crítico only.
|
||||
- Não recebe: board, inline edit, jobs, advanced search, completion doc WO-level.
|
||||
- Schema: colunas novas Fase 0 são **nullable** — Blazor continua funcionando sem conhecer novos campos.
|
||||
- Conflito Blazor vs SHOC: **SHOC vence**; Blazor não deve escrever campos board após go-live Fase 1.
|
||||
- Smoke test EF obrigatório pós-migration (ver checklist smoke).
|
||||
|
||||
---
|
||||
|
||||
## 4. Política Vendor Portal
|
||||
|
||||
- Dispatch intacto; zero regressão checklist/signoff/verify.
|
||||
- Vendor não muta WorkOrder core — apenas Dispatch e comments associados.
|
||||
- Audit EventType = `Vendor` para mutações portal.
|
||||
|
||||
---
|
||||
|
||||
## 5. Política Sync/Lambda
|
||||
|
||||
- Upsert por `ExternalWorkOrderId` (idempotente).
|
||||
- Field-level merge conforme Field Ownership Matrix.
|
||||
- Campo com ManualEditFlag → skip + audit `SyncRejected`.
|
||||
- Cutover Lambda → API direta: Fase 7.
|
||||
|
||||
---
|
||||
|
||||
## 6. Regras de conflito (resumo)
|
||||
|
||||
| Cenário | Vencedor |
|
||||
|---------|----------|
|
||||
| SHOC vs SHOC (mesmo campo) | 409 RowVersion; último commit válido |
|
||||
| SHOC vs Sync (mesmo campo) | SHOC se ManualEditFlag |
|
||||
| SHOC vs Sync (campos distintos) | Merge |
|
||||
| Vendor vs Sync | Domínios separados |
|
||||
| SHOC vs Vendor (mesmo conceito) | Impossível por design (vendor = Dispatch) |
|
||||
| Blazor vs SHOC | SHOC vence; Blazor congelado |
|
||||
|
||||
---
|
||||
|
||||
## 7. Critérios de aceite
|
||||
|
||||
- [ ] Matriz por ator revisada por CTO
|
||||
- [ ] PO confirma política Blazor sunset
|
||||
- [ ] Sync merge referencia Field Ownership Matrix
|
||||
|
||||
**Assinatura CTO:** _________________ Data: _______
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
# Database Drift Report — SQL vs EF
|
||||
|
||||
**Fase:** 0
|
||||
**Data:** 2026-06-24
|
||||
**EF Snapshot:** `Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs`
|
||||
**Script legado:** `Api.SeaHavenIndustries/db.txt`
|
||||
|
||||
---
|
||||
|
||||
## 1. Metodologia
|
||||
|
||||
1. Colunas EF extraídas do `ApplicationDbContextModelSnapshot` (entidade `WorkOrder`).
|
||||
2. Colunas SQL legado extraídas de `db.txt` DDL `[WorkOrders]`.
|
||||
3. Classificação: **mapear**, **deprecar**, **satélite**, **OK**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Tabela workOrders — drift conhecido
|
||||
|
||||
| Coluna SQL (legado) | No EF? | Classificação | Ação Fase 0 |
|
||||
|---------------------|--------|---------------|-------------|
|
||||
| WorkOrderType | **Não** | mapear | Adicionar `WorkOrderType` int nullable → enum |
|
||||
| AvettaTask | **Não** | inventariar | Adicionar coluna nullable; uso TBD com PO |
|
||||
| AssignDate | **Não** | inventariar | Adicionar coluna nullable; possível alias AssignDate tracking |
|
||||
| Nome tabela WorkOrders vs workOrders | EF usa `workOrders` | OK | Manter EF naming; SQL Server case-insensitive |
|
||||
|
||||
---
|
||||
|
||||
## 3. Colunas EF (workOrders) — baseline
|
||||
|
||||
Presentes no snapshot e mapeadas:
|
||||
|
||||
InternalWONumber, ExternalWorkOrderId, WorkerOrderNumber, WorkerOrderTitle, Description, Customer, SiteCode, Building, Severity, DateReported, ScheduledStart, ScheduledDate, CompletedDate, Source, SourceEmailS3Key, Problem, Trade, SubTrade, VendorNTE, AssignTo, DueDate, Priority, Status, LocationId, PO, TT, Attachments, BeforPhoto*, AfterPhoto*, SignOff*, istemplate, audit fields (CreatedDate, IsDeleted, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 4. Novas colunas Fase 0 (migration)
|
||||
|
||||
| Coluna | Tipo | Slice |
|
||||
|--------|------|-------|
|
||||
| LifecycleStatus | int nullable | Core |
|
||||
| LegacyStatus | nvarchar (cópia Status) | Core |
|
||||
| WorkOrderType | int nullable | Core |
|
||||
| PrimaryDispatchId | int nullable FK | Core |
|
||||
| RowVersion | rowversion | Core |
|
||||
| TargetWeek | date nullable | Scheduling |
|
||||
| ScheduledEnd | datetime2 nullable | Scheduling |
|
||||
| ScheduleWeekOnly | bit nullable | Scheduling |
|
||||
| OriginalWeek | date nullable | Tracking |
|
||||
| OriginalDate | date nullable | Tracking |
|
||||
| RescheduleCount | int default 0 | Analytics |
|
||||
| CarriedOver | int default 0 | Analytics |
|
||||
| DocStatus | int nullable | Completion |
|
||||
| AvettaTask | nvarchar max nullable | Legado SQL |
|
||||
| AssignDate | date nullable | Legado SQL |
|
||||
|
||||
---
|
||||
|
||||
## 5. Dispatch
|
||||
|
||||
| Item | EF | Ação Fase 0 |
|
||||
|------|-----|-------------|
|
||||
| RowVersion | Ausente | Adicionar |
|
||||
| Demais colunas | OK | Manter |
|
||||
|
||||
---
|
||||
|
||||
## 6. WorkOrderAuditLog
|
||||
|
||||
| Coluna | EF atual | Ação Fase 0 |
|
||||
|--------|----------|-------------|
|
||||
| EventType | Ausente | Adicionar |
|
||||
| ActorType | Ausente | Adicionar |
|
||||
| DispatchId | Ausente | Adicionar nullable |
|
||||
| CorrelationId | Ausente | Adicionar nullable |
|
||||
|
||||
---
|
||||
|
||||
## 7. WorkOrderContacts
|
||||
|
||||
| Coluna | EF | Ação Fase 0 |
|
||||
|--------|-----|-------------|
|
||||
| Notes | Ausente | Adicionar nvarchar nullable (pocNotes) |
|
||||
|
||||
---
|
||||
|
||||
## 8. ApplicationUser (AspNetUsers)
|
||||
|
||||
| Coluna | Ação Fase 0 |
|
||||
|--------|-------------|
|
||||
| Initials | nvarchar(8) nullable |
|
||||
| Color | nvarchar(16) nullable |
|
||||
|
||||
---
|
||||
|
||||
## 9. Tabelas revisadas — sem drift crítico adicional
|
||||
|
||||
- `Comments` — OK
|
||||
- `Locations` — OK (SiteCode via WO.SiteCode)
|
||||
- `DispatchWorkOrders` — OK
|
||||
|
||||
---
|
||||
|
||||
## 10. Recomendações
|
||||
|
||||
1. **Eliminar dual-path:** `db.txt` catch-up não deve ser usado após migration EF Phase0; documentar em runbook.
|
||||
2. **WorkOrderType:** mapear valores SQL existentes → enum na migration data script (dry-run).
|
||||
3. **AvettaTask / AssignDate:** manter nullable; PO valida uso antes de exposição board.
|
||||
|
||||
**Gate:** Diff assinado por Backend antes de merge migration.
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
# Field Ownership Matrix — Work Orders
|
||||
|
||||
**Fase:** 0
|
||||
**Depende de:** [data-ownership-model.md](data-ownership-model.md)
|
||||
**Status:** Draft para sign-off CTO + PO
|
||||
|
||||
---
|
||||
|
||||
## Legenda
|
||||
|
||||
- **Owner escritor:** ator autorizado a criar/alterar o valor canônico.
|
||||
- **Sync sobrescreve?:** se ingest DynamoDB pode substituir após WO existir.
|
||||
- **ManualEditFlag:** primeira edição SHOC bloqueia Sync no campo (ver [manual-edit-flag-design.md](manual-edit-flag-design.md)).
|
||||
|
||||
---
|
||||
|
||||
## Matriz completa
|
||||
|
||||
| Campo | Agregado | Owner escritor | Sync sobrescreve? | Conflito SHOC vs Sync |
|
||||
|-------|----------|----------------|-------------------|----------------------|
|
||||
| LifecycleStatus | Core | SHOC | Não após 1ª edição SHOC | SHOC vence |
|
||||
| LegacyStatus | Core | — (read-only) | Não | N/A |
|
||||
| AssignTo | Core | SHOC | Não após ManualEditFlag | SHOC vence |
|
||||
| ScheduledDate | Scheduling | SHOC | Não | SHOC vence |
|
||||
| ScheduledStart | Scheduling | SHOC | Parcial (ingest inicial) | SHOC após flag |
|
||||
| ScheduledEnd | Scheduling | SHOC | Não | SHOC vence |
|
||||
| TargetWeek | Scheduling | SHOC | Não | SHOC vence |
|
||||
| ScheduleWeekOnly | Scheduling | SHOC | Não | SHOC vence |
|
||||
| DueDate | Core | SHOC + Sync | Sim se nunca editado SHOC | SHOC se ManualEditFlag |
|
||||
| Description | Core | SHOC + Sync | Sim se nunca editado SHOC | SHOC se ManualEditFlag |
|
||||
| WorkerOrderTitle | Core | SHOC + Sync | Sim se nunca editado SHOC | SHOC se ManualEditFlag |
|
||||
| SiteCode | Core | SHOC + Sync | Sim na criação; não após SHOC | SHOC após flag |
|
||||
| Building | Core | SHOC + Sync | Sim na criação | SHOC após flag |
|
||||
| LocationId | Core | SHOC + Sync | Sim na criação | SHOC após flag |
|
||||
| WorkOrderType | Core | SHOC + Sync | Sim na criação ingest | SHOC após flag |
|
||||
| InternalWONumber / WoNumber | Core | SHOC (Sync só na criação) | Não | SHOC only |
|
||||
| ExternalWorkOrderId | Core | Sync | Sim (idempotente key) | Sync only |
|
||||
| VendorId / VendorName | Dispatch | SHOC + Vendor | Não | Domínios separados |
|
||||
| Dispatch Status | Dispatch | Vendor + SHOC | Não | Por contexto |
|
||||
| Checklist / Signoff | Dispatch | Vendor | Não | Vendor vence |
|
||||
| DocStatus | Completion | SHOC | Não | SHOC only |
|
||||
| RescheduleCount | Analytics | Domain events | Não | N/A |
|
||||
| CarriedOver | Analytics | Jobs (WeekRolled) | Não | N/A |
|
||||
| OriginalWeek / OriginalDate | Tracking | SHOC (set-once) | Não | SHOC vence |
|
||||
| POC / WorkOrderContacts.Notes | Contacts | SHOC | Não | SHOC vence |
|
||||
| Priority / Severity | Core | SHOC + Sync | Sim se nunca editado | SHOC após flag |
|
||||
| Status (legado string) | Core | — (deprecated write) | Não | Migrar para LifecycleStatus |
|
||||
| Trade / Problem / SubTrade | Core | SHOC | Parcial Sync ingest | SHOC após flag |
|
||||
| Customer / Source | Core | Sync + SHOC | Sim na criação | SHOC após flag |
|
||||
| Attachments / Photos | Core | SHOC + Vendor | Não overwrite mútuo | Por domínio |
|
||||
|
||||
---
|
||||
|
||||
## Campos board (14 colunas) — ownership resumido
|
||||
|
||||
| Coluna board | Fonte | Owner |
|
||||
|--------------|-------|-------|
|
||||
| WO# | Core.InternalWONumber | SHOC |
|
||||
| Type | Core.WorkOrderType | SHOC |
|
||||
| Site | Core.SiteCode | SHOC |
|
||||
| Status | Core.LifecycleStatus | SHOC |
|
||||
| Assignee | Core.AssignTo | SHOC |
|
||||
| Due | Core.DueDate | SHOC + Sync |
|
||||
| Scheduled | Scheduling.ScheduledDate | SHOC |
|
||||
| Vendor | Dispatch (primário) | SHOC via Dispatch |
|
||||
| Appt | Dispatch.ScheduledDate | Dispatch |
|
||||
| Past Due | Derivado | — |
|
||||
| Carried | Analytics.CarriedOver | Jobs |
|
||||
| Reschedule | Analytics.RescheduleCount | Domain |
|
||||
| Doc | Completion.DocStatus | SHOC |
|
||||
| Actions | — | SHOC UI |
|
||||
|
||||
---
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [ ] PO valida campos board vs matriz
|
||||
- [ ] Sync merge implementado referencia esta matriz
|
||||
- [ ] ManualEditFlag design aprovado
|
||||
|
||||
**Assinaturas**
|
||||
|
||||
| Papel | Data |
|
||||
|-------|------|
|
||||
| CTO | |
|
||||
| PO | |
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
# ManualEditFlag — Design
|
||||
|
||||
**Fase:** 0
|
||||
**Implementação:** tabela satélite `WorkOrderFieldLocks`
|
||||
|
||||
---
|
||||
|
||||
## 1. Problema
|
||||
|
||||
Sync/Lambda faz upsert overwrite em campos que dispatchers editam no SHOC. Precisamos saber, por campo, se houve edição manual SHOC para rejeitar ingest conflitante.
|
||||
|
||||
---
|
||||
|
||||
## 2. Solução escolhida: tabela satélite
|
||||
|
||||
```text
|
||||
WorkOrderFieldLocks
|
||||
Id (PK)
|
||||
WorkOrderId (FK)
|
||||
FieldName (string, max 64)
|
||||
LockedAt (UTC)
|
||||
LockedByUserId (nullable — null = system/bootstrap)
|
||||
UNIQUE (WorkOrderId, FieldName)
|
||||
```
|
||||
|
||||
**Alternativa descartada:** bitmask — difícil de auditar e estender.
|
||||
|
||||
---
|
||||
|
||||
## 3. Comportamento
|
||||
|
||||
| Evento | Ação |
|
||||
|--------|------|
|
||||
| SHOC edita campo X pela 1ª vez | INSERT lock (WorkOrderId, FieldName) |
|
||||
| Sync tenta atualizar campo X com lock | Skip update; audit `SyncRejected` |
|
||||
| Sync atualiza campo Y sem lock | Apply merge normal |
|
||||
| WO novo (Sync create) | Sem locks; full ingest |
|
||||
| Blazor edita campo legado | **Não** cria lock Fase 0 (gap conhecido; Blazor congelado) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Campos elegíveis a lock (SHOC writers)
|
||||
|
||||
Definidos em `SyncFieldMergePolicy.ShocOwnedFields`:
|
||||
|
||||
- LifecycleStatus, AssignTo, ScheduledDate, ScheduledEnd, TargetWeek, ScheduleWeekOnly
|
||||
- DueDate, Description, WorkerOrderTitle, SiteCode, Building, LocationId
|
||||
- WorkOrderType, DocStatus, Trade, Problem, Priority, InternalWONumber
|
||||
|
||||
---
|
||||
|
||||
## 5. Audit em rejeição
|
||||
|
||||
```json
|
||||
{
|
||||
"eventType": "Sync",
|
||||
"action": "SyncRejected",
|
||||
"fieldName": "DueDate",
|
||||
"oldValue": "2026-06-01",
|
||||
"newValue": "2026-06-15",
|
||||
"actorType": "Sync"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. API interna
|
||||
|
||||
```csharp
|
||||
interface IWorkOrderFieldLockService
|
||||
{
|
||||
Task LockFieldAsync(int workOrderId, string fieldName, string? userId);
|
||||
Task<bool> IsLockedAsync(int workOrderId, string fieldName);
|
||||
}
|
||||
```
|
||||
|
||||
Implementação: `WorkOrderFieldLockService` em `SeaHaven.Services`.
|
||||
|
||||
Lock criado automaticamente por `IWorkOrderAuditService` em eventos `Manual` + `FieldChanged` / `StatusChanged`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Critérios de aceite
|
||||
|
||||
- [ ] Tabela criada na migration Phase0
|
||||
- [ ] Sync consulta locks antes de overwrite
|
||||
- [ ] SyncRejected aparece no audit log
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# Migration Dry-Run Report — Phase 0
|
||||
|
||||
**Migration:** `20260624163145_Phase0_DomainFoundation`
|
||||
**Status:** Pronto para execução em staging
|
||||
|
||||
---
|
||||
|
||||
## 1. Procedimento
|
||||
|
||||
1. Restore backup produção → staging espelho
|
||||
2. Snapshot pré-migration:
|
||||
```sql
|
||||
SELECT Status, COUNT(*) FROM workOrders GROUP BY Status;
|
||||
SELECT COUNT(*) AS Total FROM workOrders;
|
||||
```
|
||||
3. Executar:
|
||||
```powershell
|
||||
dotnet ef database update --project Data.SeaHavenIndustries --startup-project Api.SeaHavenIndustries
|
||||
```
|
||||
4. Validar pós-migration:
|
||||
```sql
|
||||
SELECT COUNT(*) FROM workOrders WHERE LegacyStatus IS NOT NULL;
|
||||
SELECT LifecycleStatus, COUNT(*) FROM workOrders GROUP BY LifecycleStatus;
|
||||
SELECT COUNT(*) FROM workOrders WHERE PrimaryDispatchId IS NOT NULL;
|
||||
```
|
||||
5. Smoke Blazor + API legado + Sync sample
|
||||
6. Rollback (se falha):
|
||||
```powershell
|
||||
dotnet ef database update 20260417195316_AddDispatchVerification --project Data.SeaHavenIndustries --startup-project Api.SeaHavenIndustries
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Data scripts incluídos na migration
|
||||
|
||||
- `LegacyStatus` ← cópia de `Status`
|
||||
- `LifecycleStatus` ← mapeamento legado (Open→Incomplete, etc.)
|
||||
- `PrimaryDispatchId` ← dispatch mais recente por WO
|
||||
|
||||
---
|
||||
|
||||
## 3. Resultados (preencher em staging)
|
||||
|
||||
| Métrica | Antes | Depois |
|
||||
|---------|-------|--------|
|
||||
| Total WOs | | |
|
||||
| Com LegacyStatus | | |
|
||||
| Com LifecycleStatus | | |
|
||||
| Com PrimaryDispatchId | | |
|
||||
| Rollback testado | | Sim/Não |
|
||||
|
||||
---
|
||||
|
||||
## 4. Notas
|
||||
|
||||
A migration inclui reconciliação de drift em `Locations`, `Regions`, `Departments` detectada pelo EF — revisar impacto em staging antes de produção.
|
||||
|
||||
**WO# normalização 11 dígitos:** documentada para Fase 2; não executada nesta migration.
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# Phase 0 — Gates Sign-Off Checklist
|
||||
|
||||
**Programa:** Work Orders Board
|
||||
**Fase:** 0 — Domain Foundation + Governança
|
||||
|
||||
---
|
||||
|
||||
## 9 Gates bloqueantes
|
||||
|
||||
| # | Gate | Artefato | Status |
|
||||
|---|------|----------|--------|
|
||||
| 1 | DAR aprovado | [dar-domain-architecture-review.md](dar-domain-architecture-review.md) | Documentado |
|
||||
| 2 | Data Ownership Model | [data-ownership-model.md](data-ownership-model.md) | Documentado |
|
||||
| 3 | Field Ownership Matrix | [field-ownership-matrix.md](field-ownership-matrix.md) | Documentado |
|
||||
| 4 | Audit Event Contract | [audit-event-contract.md](audit-event-contract.md) | Implementado baseline |
|
||||
| 5 | Volume Discovery | [volume-discovery-report.md](volume-discovery-report.md) | Queries prontas; tier S provisório |
|
||||
| 6 | Database drift | [database-drift-report.md](database-drift-report.md) | Documentado |
|
||||
| 7 | Spike vendor/dispatch | [adr-vendor-source-of-truth.md](adr-vendor-source-of-truth.md) | ADR + query |
|
||||
| 8 | Migration dry-run | [migration-dry-run-report.md](migration-dry-run-report.md) | Procedimento pronto |
|
||||
| 9 | RowVersion multi-agregado | WO + Dispatch + ConcurrencyExceptionFilter | Implementado |
|
||||
|
||||
---
|
||||
|
||||
## Implementação código (Fase 0)
|
||||
|
||||
- Migration `Phase0_DomainFoundation`
|
||||
- Enums: LifecycleStatus, WorkOrderType, DocStatus, AuditEventType, etc.
|
||||
- `IWorkOrderAuditService`, `IWorkOrderFieldLockService`, `ISyncFieldMergePolicy`
|
||||
- `[Authorize]` reativado em WorkOrderController
|
||||
- Projeto `SeaHavenIndustries.Tests`
|
||||
|
||||
---
|
||||
|
||||
## GO Fase 1 — pendente
|
||||
|
||||
- [ ] Sign-off CTO/PO nos artefatos
|
||||
- [ ] Dry-run staging executado
|
||||
- [ ] Volume tier confirmado em staging/prod
|
||||
- [ ] Smoke checklist verde
|
||||
|
||||
**Assinatura GO Fase 1**
|
||||
|
||||
| Papel | Nome | Data |
|
||||
|-------|------|------|
|
||||
| CTO | | |
|
||||
| PO | | |
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# RowVersion Multi-Agregado — Design
|
||||
|
||||
**Fase:** 0 — Gate #9
|
||||
|
||||
---
|
||||
|
||||
## Agregados
|
||||
|
||||
| Agregado | Coluna | Comportamento |
|
||||
|----------|--------|---------------|
|
||||
| WorkOrder | `RowVersion` rowversion | Bump em mutação WO (schedule, status, assignment) |
|
||||
| Dispatch | `RowVersion` rowversion | Bump em mutação vendor/checklist/signoff |
|
||||
|
||||
---
|
||||
|
||||
## HTTP 409
|
||||
|
||||
`ConcurrencyExceptionFilter` captura `DbUpdateConcurrencyException` e retorna:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "Conflict",
|
||||
"message": "The record was modified by another user. Refresh and retry.",
|
||||
"code": 409
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Regras (Fase 2 testes completos)
|
||||
|
||||
- SHOC edita ScheduledDate → bump WO.RowVersion
|
||||
- SHOC edita vendor → bump Dispatch.RowVersion
|
||||
- Vendor edita checklist → bump Dispatch.RowVersion only
|
||||
|
||||
Testes de concorrência paralela: Fase 2 (`Concurrency integration tests` backlog #27).
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
# Smoke Test Checklist — Multi-Consumidor (Fase 0)
|
||||
|
||||
**Ambiente:** Docker local (`docker compose up -d` + `scripts/setup-local-db.ps1`)
|
||||
|
||||
---
|
||||
|
||||
## Blazor EF
|
||||
|
||||
| # | Teste | Pass |
|
||||
|---|-------|------|
|
||||
| 1 | `/workorderlist` carrega sem exception | [ ] |
|
||||
| 2 | Abrir detalhe WO existente | [ ] |
|
||||
| 3 | Campos novos nullable não quebram binding | [ ] |
|
||||
|
||||
---
|
||||
|
||||
## API REST (legado)
|
||||
|
||||
| # | Teste | Pass |
|
||||
|---|-------|------|
|
||||
| 1 | POST login → JWT | [ ] |
|
||||
| 2 | GET work order list com Bearer token | [ ] |
|
||||
| 3 | POST ChangeStatus com audit no log | [ ] |
|
||||
| 4 | POST ChangeAssignment.tex Assignment com audit | [ ] |
|
||||
| 5 | 401 sem token (auth reativada) | [ ] |
|
||||
|
||||
---
|
||||
|
||||
## Sync
|
||||
|
||||
| # | Teste | Pass |
|
||||
|---|-------|------|
|
||||
| 1 | POST `api/Sync/WorkOrders` com JWT (mock/staging DynamoDB) | [ ] |
|
||||
| 2 | Upsert idempotente por ExternalWorkOrderId | [ ] |
|
||||
| 3 | Campo locked → SyncRejected no audit | [ ] |
|
||||
|
||||
---
|
||||
|
||||
## Vendor Portal
|
||||
|
||||
| # | Teste | Pass |
|
||||
|---|-------|------|
|
||||
| 1 | Checklist update flow | [ ] |
|
||||
| 2 | Signoff flow | [ ] |
|
||||
| 3 | Dispatch RowVersion presente (schema) | [ ] |
|
||||
|
||||
---
|
||||
|
||||
## Automação local
|
||||
|
||||
```powershell
|
||||
dotnet test SeaHavenIndustries.Tests
|
||||
dotnet build
|
||||
./scripts/setup-local-db.ps1
|
||||
```
|
||||
|
||||
Testes unitários cobrem: LifecycleStatusMapper, audit baseline, sync merge rejection.
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
# Volume Discovery Report — Work Orders
|
||||
|
||||
**Fase:** 0 — Gate Fase 1
|
||||
**Data:** 2026-06-24
|
||||
**Ambiente:** Local dev (Docker) + queries para staging/prod
|
||||
|
||||
---
|
||||
|
||||
## 1. Objetivo
|
||||
|
||||
Definir tier S/M/L/XL antes de índices e estratégia de search (Fase 4). **Sem premissa de volume.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Queries obrigatórias
|
||||
|
||||
Executar em staging ou produção read-only:
|
||||
|
||||
```sql
|
||||
-- Total WOs
|
||||
SELECT COUNT(*) AS TotalWorkOrders
|
||||
FROM workOrders
|
||||
WHERE IsDeleted IS NULL OR IsDeleted = 0;
|
||||
|
||||
-- WOs por semana (ISO) — p95
|
||||
WITH WeeklyCounts AS (
|
||||
SELECT DATEPART(iso_week, ScheduledDate) AS IsoWeek,
|
||||
YEAR(ScheduledDate) AS IsoYear,
|
||||
COUNT(*) AS Cnt
|
||||
FROM workOrders
|
||||
WHERE ScheduledDate IS NOT NULL
|
||||
AND (IsDeleted IS NULL OR IsDeleted = 0)
|
||||
GROUP BY DATEPART(iso_week, ScheduledDate), YEAR(ScheduledDate)
|
||||
)
|
||||
SELECT MAX(Cnt) AS P95ProxyMaxPerWeek,
|
||||
AVG(Cnt * 1.0) AS AvgPerWeek
|
||||
FROM WeeklyCounts;
|
||||
|
||||
-- Unscheduled (não terminal)
|
||||
SELECT COUNT(*) AS UnscheduledCount
|
||||
FROM workOrders
|
||||
WHERE ScheduledDate IS NULL
|
||||
AND (IsDeleted IS NULL OR IsDeleted = 0)
|
||||
AND Status NOT IN ('Completed', 'Complete', 'Cancelled', 'Canceled');
|
||||
|
||||
-- Crescimento mensal
|
||||
SELECT YEAR(CreatedDate) AS Y, MONTH(CreatedDate) AS M, COUNT(*) AS Cnt
|
||||
FROM workOrders
|
||||
WHERE CreatedDate IS NOT NULL
|
||||
GROUP BY YEAR(CreatedDate), MONTH(CreatedDate)
|
||||
ORDER BY Y DESC, M DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Resultados
|
||||
|
||||
| Métrica | Local Docker (dev) | Staging | Produção |
|
||||
|---------|-------------------|---------|----------|
|
||||
| Total WOs | *executar após seed* | _pendente acesso_ | _pendente acesso_ |
|
||||
| WOs/semana p95 | — | _pendente_ | _pendente_ |
|
||||
| Unscheduled | — | _pendente_ | _pendente_ |
|
||||
| Crescimento mensal | — | _pendente_ | _pendente_ |
|
||||
|
||||
**Nota:** Ambiente local inicia vazio. Tier provisório para desenvolvimento: **S** (< 25k). Confirmar com query em staging antes do GO Fase 1.
|
||||
|
||||
---
|
||||
|
||||
## 4. Classificação tier (§10.2 roadmap)
|
||||
|
||||
| Tier | Total WOs | Listagem semanal | Advanced search |
|
||||
|------|-----------|------------------|-----------------|
|
||||
| **S** | < 25k | Índices simples | SQL filtros compostos |
|
||||
| **M** | 25k–250k | Índices covering | Paginação obrigatória |
|
||||
| **L** | 250k–1M | Read model candidato | Full-text |
|
||||
| **XL** | > 1M | Materialized view | Search dedicado (ARB) |
|
||||
|
||||
**Tier definido para Fase 1:** **S (provisório dev)** — revisar ao obter métricas staging.
|
||||
|
||||
---
|
||||
|
||||
## 5. Impacto Fase 0–1
|
||||
|
||||
- Fases 1–3: índices conservadores compatíveis com qualquer tier.
|
||||
- Fase 4: implementação search conforme tier confirmado + load test.
|
||||
|
||||
---
|
||||
|
||||
## 6. Script local
|
||||
|
||||
```powershell
|
||||
./scripts/setup-local-db.ps1
|
||||
# Executar queries acima via sqlcmd ou DBCode contra localhost:1433
|
||||
```
|
||||
|
||||
**Gate:** Preencher coluna Staging/Produção antes do GO Fase 1.
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
# 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 | Segunda-feira da semana (`YYYY-MM-DD`) |
|
||||
| `weekEnd` | Não | Sexta-feira (default: `weekStart + 4 dias`) |
|
||||
| `dispatchers` | Não | Lista de GUIDs; use `__unassigned__` para não atribuídos |
|
||||
| `myWorkOrders` | Não | `true` → filtra `AssignTo == usuário logado` |
|
||||
| `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
|
||||
|
||||
### `GET /api/workorders/lookups/dispatchers`
|
||||
|
||||
Lista dispatchers para filtros do board.
|
||||
|
||||
```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.
|
||||
|
||||
---
|
||||
|
||||
## 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/20260624180343_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).
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
# Fase 3 — Criação e Cancelamento
|
||||
|
||||
**Programa:** Work Orders Board
|
||||
**Objetivo:** Criação wizard/inline via contrato do board SHOC, cancelamento soft dedicado, ManualEditFlag na criação, e delete físico restrito a Admin.
|
||||
|
||||
**Depende de:** [Fase 0](../phase-0/README.md), [Fase 1](../phase-1/README.md), [Fase 2](../phase-2/README.md)
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### `POST /api/workorders/board`
|
||||
|
||||
Cria uma WO com contrato alinhado ao board. Suporta wizard (payload completo) e inline row (subset mínimo).
|
||||
|
||||
**Autenticação:** Bearer JWT (`[Authorize]`)
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"woNumber": "12345",
|
||||
"workOrderType": "PM",
|
||||
"siteCode": "BK5",
|
||||
"assignTo": "dispatcher-guid",
|
||||
"dueDate": "2026-07-01",
|
||||
"scheduledDate": "2026-06-25",
|
||||
"targetWeek": "2026-06-22",
|
||||
"scheduleWeekOnly": false,
|
||||
"vendorId": 5,
|
||||
"apptDate": "2026-06-26",
|
||||
"apptTime": "09:00 – 11:00",
|
||||
"docStatus": "No",
|
||||
"description": "Leak in break room",
|
||||
"trade": "HVAC PM",
|
||||
"locationId": 12,
|
||||
"pocContactId": 3,
|
||||
"pocNotes": "Call before arrival"
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Obrigatório | Descrição |
|
||||
|-------|-------------|-----------|
|
||||
| `workOrderType` | Sim | Enum `WorkOrderType` |
|
||||
| `siteCode` | Sim | Código do site |
|
||||
| `woNumber` | Não | Se omitido, auto-gera sequencial normalizado 11 dígitos |
|
||||
| `scheduleWeekOnly` | Não | Se `true`, `targetWeek` é obrigatório |
|
||||
| `vendorId` | Condicional | Obrigatório quando `apptDate` ou `apptTime` informados |
|
||||
|
||||
**Responses:**
|
||||
|
||||
| HTTP | Body | Quando |
|
||||
|------|------|--------|
|
||||
| 200 | `WorkOrderBoardRowDto` | Sucesso — row pronto para inserir no board |
|
||||
| 400 | `Response` | Validação FluentValidation |
|
||||
| 409 | `WorkOrderBoardValidationErrorDto` | WO# duplicado |
|
||||
| 422 | `WorkOrderBoardValidationErrorDto` | Regra de domínio |
|
||||
|
||||
**Exemplo (inline mínimo):**
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"workOrderType":"PM","siteCode":"BK5"}' \
|
||||
"https://localhost:5001/api/workorders/board"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/workorders/{id}/cancel`
|
||||
|
||||
Soft cancel — transição para `LifecycleStatus.Canceled` com WO read-only.
|
||||
|
||||
**Autenticação:** Bearer JWT
|
||||
|
||||
**Responses:**
|
||||
|
||||
| HTTP | Body | Quando |
|
||||
|------|------|--------|
|
||||
| 200 | `WorkOrderBoardRowDto` | Cancelado (ou já estava cancelado — idempotente) |
|
||||
| 422 | `WorkOrderBoardValidationErrorDto` | WO `Complete` ou `Closed` |
|
||||
|
||||
**Exemplo:**
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Authorization: Bearer <token>" \
|
||||
"https://localhost:5001/api/workorders/42/cancel"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Regras de domínio na criação
|
||||
|
||||
| Regra | Comportamento |
|
||||
|-------|---------------|
|
||||
| Status inicial | `Incomplete` |
|
||||
| Auto-schedule | `scheduledDate` + `assignTo` → `Scheduled` |
|
||||
| Set-once tracking | Primeira `scheduledDate` → `OriginalDate` / `OriginalWeek` |
|
||||
| Week-only | `scheduleWeekOnly=true` + `targetWeek` → aparece na semana no GET board |
|
||||
| WO# | Manual normalizado 11 dígitos; auto-gerado se omitido |
|
||||
| Vendor | Cria dispatch primário quando `vendorId` informado |
|
||||
| ManualEditFlag | Field locks criados para cada campo SHOC preenchido |
|
||||
|
||||
---
|
||||
|
||||
## Soft cancel vs hard delete
|
||||
|
||||
| Operação | Endpoint | Quem | Efeito |
|
||||
|----------|----------|------|--------|
|
||||
| Soft cancel | `POST /{id}/cancel` | Dispatcher | Status `Canceled`, WO permanece, PATCH bloqueado (422) |
|
||||
| Hard delete | `DELETE DeleteWorkorder` | **Admin only** | Remove registro, anexos e contatos |
|
||||
|
||||
O legado `POST AddWorkorder` (form/Blazor) permanece inalterado.
|
||||
|
||||
---
|
||||
|
||||
## ManualEditFlag na criação
|
||||
|
||||
Campos preenchidos na criação SHOC recebem lock em `WorkOrderFieldLocks` via audit `FieldChanged`. Sync subsequente em campo lockado gera `SyncRejected` (ver [manual-edit-flag-design.md](../phase-0/manual-edit-flag-design.md)).
|
||||
|
||||
WO criada via Sync/Lambda continua sem locks até edição SHOC.
|
||||
|
||||
---
|
||||
|
||||
## Audit
|
||||
|
||||
| Evento | Action |
|
||||
|--------|--------|
|
||||
| Criação | 1× `Create` + `FieldChanged` por campo preenchido |
|
||||
| Auto-schedule na criação | `StatusChanged` adicional |
|
||||
| Cancel | `StatusChanged` → `Canceled` |
|
||||
| Hard delete Admin | `Delete` |
|
||||
|
||||
---
|
||||
|
||||
## Código entregue
|
||||
|
||||
| Camada | Arquivo |
|
||||
|--------|---------|
|
||||
| DTOs | `SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs` |
|
||||
| Validação | `SeaHaven.Services/Validation/WorkOrderBoardCreateValidation.cs` |
|
||||
| Scheduling compartilhado | `SeaHaven.Services/Helpers/WorkOrderBoardFieldMutations.cs` |
|
||||
| Create service | `SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs` |
|
||||
| Cancel service | `SeaHaven.Services/Implementation/WorkOrderBoardCancelService.cs` |
|
||||
| Audit | `IWorkOrderAuditService.StageCreated`, `LogCreatedAsync` |
|
||||
| API | `WorkOrderController` — `POST board`, `POST {id}/cancel`, `DELETE` Admin |
|
||||
| Testes | `WorkOrderBoardCreateServiceTests`, `WorkOrderBoardCancelServiceTests`, `WorkOrderBoardCreateSyncLockTests` |
|
||||
|
||||
---
|
||||
|
||||
## Gates de aceite
|
||||
|
||||
### Desenvolvimento
|
||||
|
||||
- [x] `POST /api/workorders/board` — Incomplete / auto-schedule / week-only
|
||||
- [x] WO# auto ou manual 11 dígitos + unicidade (409)
|
||||
- [x] Vendor/dispatch primário na criação
|
||||
- [x] Field locks + SyncRejected pós-create SHOC
|
||||
- [x] `POST /{id}/cancel` idempotente; PATCH bloqueado após cancel
|
||||
- [x] `DELETE DeleteWorkorder` restrito a Admin
|
||||
- [x] Legado `AddWorkorder` inalterado
|
||||
- [x] 68 testes unitários total (17 Fase 3 + 51 Fases 0–2)
|
||||
|
||||
### Staging (pendente)
|
||||
|
||||
- [ ] UAT dispatcher: wizard + inline row
|
||||
- [ ] Smoke Sync durante criação SHOC
|
||||
- [ ] Confirmar navegação FE para semana do WO criado
|
||||
|
||||
---
|
||||
|
||||
## Próximo passo
|
||||
|
||||
**Fase 4** — Busca contextual na semana + advanced search cross-week conforme tier volume.
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
# Fase 4 — Search contextual + Advanced Search
|
||||
|
||||
**Programa:** Work Orders Board
|
||||
**Objetivo:** Hardening da busca contextual no board semanal, endpoint de advanced search cross-week (Tier S), índices de suporte e harness de load test.
|
||||
|
||||
**Depende de:** [Fase 0](../phase-0/README.md), [Fase 1](../phase-1/README.md), [Fase 2](../phase-2/README.md), [Fase 3](../phase-3/README.md)
|
||||
|
||||
**Tier vigente (dev):** **S (provisório)** — ver [search-tier-decision.md](./search-tier-decision.md)
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### `GET /api/workorders/board?search=` (hardening)
|
||||
|
||||
Busca contextual na janela semanal. Campos cobertos:
|
||||
|
||||
| Campo | Origem |
|
||||
|-------|--------|
|
||||
| Site | `SiteCode` |
|
||||
| WO# | `InternalWONumber`, `WorkerOrderNumber` |
|
||||
| Location | `Locations.Name` |
|
||||
| Dispatcher | `AssignToUser` nome |
|
||||
| PM | `Trade`, `Problem` |
|
||||
| Vendor | `PrimaryDispatch.Vendor.CompanyName` |
|
||||
| Tech | `PrimaryDispatch.Vendor.ContactName` |
|
||||
| Status legado | `Status` |
|
||||
| Lifecycle | label enum (`Scheduled`, `In Progress`, etc.) |
|
||||
|
||||
**Regras:**
|
||||
|
||||
- `search` com trim; ignorado se vazio ou < 2 caracteres
|
||||
- `counts.total` — agendadas na semana **sem** search
|
||||
- `counts.returned` — agendadas **com** search
|
||||
|
||||
### `GET /api/workorders/board/search`
|
||||
|
||||
Advanced search cross-week paginado.
|
||||
|
||||
**Autenticação:** Bearer JWT
|
||||
|
||||
**Query params:**
|
||||
|
||||
| Param | Tipo | Descrição |
|
||||
|-------|------|-----------|
|
||||
| `search` | string | Texto livre (mesmos campos do contextual) |
|
||||
| `datePreset` | enum | `thisWeek`, `lastWeek`, `thisMonth`, `last3Months`, `nextWeek`, `nextMonth`, `custom` |
|
||||
| `dateFrom` / `dateTo` | date | Obrigatórios se `datePreset=custom` |
|
||||
| `sites` | string[] | `SiteCode` |
|
||||
| `types` | WorkOrderType[] | Tipo WO |
|
||||
| `dispatchers` | string[] | GUIDs + `__unassigned__` |
|
||||
| `statuses` | LifecycleStatus[] | Status operacional |
|
||||
| `pmTypes` | string[] | Match em `Trade`/`Problem` (contains, case-insensitive) |
|
||||
| `vendorIds` | int[] | Via `PrimaryDispatch.VendorId` |
|
||||
| `docStatuses` | DocStatus[] | Enum `DocStatus` |
|
||||
| `myWorkOrders` | bool | Filtro usuário logado |
|
||||
| `page` | int | Default 1 |
|
||||
| `pageSize` | int | Default 50; max 100 (Tier S/M) |
|
||||
| `sortBy` | string | `scheduledDate` (default), `woNumber`, `dueDate` |
|
||||
| `sortDir` | string | `asc` / `desc` |
|
||||
|
||||
**Response:** `PagedResult<WorkOrderBoardRowDto>`
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [ /* WorkOrderBoardRowDto */ ],
|
||||
"totalCount": 120,
|
||||
"page": 1,
|
||||
"pageSize": 50,
|
||||
"totalPages": 3,
|
||||
"hasNext": true,
|
||||
"hasPrevious": false
|
||||
}
|
||||
```
|
||||
|
||||
**Exemplo:**
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <token>" \
|
||||
"https://localhost:5001/api/workorders/board/search?datePreset=thisMonth&sites=BK5&search=HVAC&page=1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Matriz preset → intervalo de datas
|
||||
|
||||
Base: **segunda-feira ISO** (`WorkOrderSearchDateRangeResolver`).
|
||||
|
||||
| Preset | Intervalo |
|
||||
|--------|-----------|
|
||||
| `thisWeek` | Seg–Dom da semana ISO corrente |
|
||||
| `lastWeek` | Seg–Dom da semana ISO anterior |
|
||||
| `nextWeek` | Seg–Dom da próxima semana ISO |
|
||||
| `thisMonth` | 1º–último dia do mês corrente |
|
||||
| `nextMonth` | 1º–último dia do mês seguinte |
|
||||
| `last3Months` | Hoje − 3 meses → hoje |
|
||||
| `custom` | `dateFrom` / `dateTo` (400 se ausentes) |
|
||||
|
||||
**Filtro de data:** `ScheduledDate` no intervalo **OU** `ScheduleWeekOnly && TargetWeek` intersectando o intervalo. Exclui templates e `IsDeleted`.
|
||||
|
||||
---
|
||||
|
||||
## Código entregue
|
||||
|
||||
| Camada | Arquivo |
|
||||
|--------|---------|
|
||||
| Search filter | `SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs` |
|
||||
| Query filters | `SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs` |
|
||||
| Projeção | `SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs` |
|
||||
| Date presets | `SeaHaven.Services/Helpers/WorkOrderSearchDateRangeResolver.cs` |
|
||||
| Advanced data | `SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs` |
|
||||
| Advanced service | `SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs` |
|
||||
| DTOs | `SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs` |
|
||||
| API | `WorkOrderController` — `GET board/search` |
|
||||
| Migration | `20260624200000_Phase4_SearchIndexes.cs` |
|
||||
| Testes | `SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs` |
|
||||
| Load test | `scripts/load-test/work-order-search.k6.js` |
|
||||
| Seed | `scripts/seed-work-orders-search.ps1` |
|
||||
|
||||
---
|
||||
|
||||
## Gates de aceite
|
||||
|
||||
### Dev (implementação)
|
||||
|
||||
- [x] Helper de search compartilhado + testes por campo
|
||||
- [x] `GET /board/search` paginado com filtros FE
|
||||
- [x] Date presets alinhados ao SHOC (ISO Monday)
|
||||
- [x] Migration índices Phase 4
|
||||
- [x] Harness k6 + seed sintético (Tier S local)
|
||||
- [x] Endpoints legados inalterados
|
||||
- [x] ~12 testes unitários novos (total ~80)
|
||||
|
||||
### Staging/prod (GO — pendente)
|
||||
|
||||
- [ ] Volume Discovery preenchido — [volume-discovery-report.md](../phase-0/volume-discovery-report.md)
|
||||
- [ ] Tier assinado — [search-tier-decision.md](./search-tier-decision.md)
|
||||
- [ ] Load test em staging com volume real
|
||||
- [ ] Latência dentro do SLO do tier confirmado
|
||||
- [ ] Smoke Portal/Blazor inalterados
|
||||
|
||||
---
|
||||
|
||||
## SLOs Tier S (load test local)
|
||||
|
||||
| Cenário | Endpoint | p95 |
|
||||
|---------|----------|-----|
|
||||
| Board sem search | `GET /board?weekStart=...` | < 800ms |
|
||||
| Board com search | `GET /board?search=BK5` | < 1000ms |
|
||||
| Advanced preset | `GET /board/search?datePreset=thisMonth` | < 1200ms |
|
||||
| Advanced multi-filter | sites + types + search | < 1500ms |
|
||||
|
||||
Ver [scripts/load-test/README.md](../../scripts/load-test/README.md).
|
||||
|
||||
---
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Full-text search (Tier L)
|
||||
- Search externo dedicado (Tier XL)
|
||||
- Lookups PM catalog (`PM_TYPES`) — backlog #17
|
||||
- Alterações em endpoints legados
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
# Search Tier Decision — Fase 4
|
||||
|
||||
**Data:** 2026-06-24
|
||||
**Status:** Provisório (dev) — aguardando Volume Discovery staging/prod
|
||||
|
||||
---
|
||||
|
||||
## Tier confirmado
|
||||
|
||||
| Ambiente | Tier | Total WOs | Decisão |
|
||||
|----------|------|-----------|---------|
|
||||
| Dev local | **S (provisório)** | < 25k sintético | Implementar SQL filtros + paginação opcional |
|
||||
| Staging | _pendente_ | _pendente acesso_ | Executar [volume-discovery-report.md](../phase-0/volume-discovery-report.md) |
|
||||
| Produção | _pendente_ | _pendente acesso_ | Executar queries read-only |
|
||||
|
||||
---
|
||||
|
||||
## Estratégia por tier
|
||||
|
||||
| Tier | Volume | Estratégia advanced search |
|
||||
|------|--------|----------------------------|
|
||||
| **S** | < 25k | SQL filtros compostos + paginação default 50, max 100 |
|
||||
| **M** | 25k–250k | Paginação **obrigatória** + índices covering |
|
||||
| **L** | 250k–1M | Spike full-text SQL Server — **não implementar sem spike** |
|
||||
| **XL** | > 1M | Documentar ARB; search externo — **não implementar** |
|
||||
|
||||
---
|
||||
|
||||
## Implementação atual (Tier S)
|
||||
|
||||
- `WorkOrderBoardSearchFilter` — filtro textual compartilhado (board + advanced)
|
||||
- `WorkOrderAdvancedSearchDataService` — query composta com `CountAsync` + `Skip/Take`
|
||||
- Índices Phase 4: `SiteCode`, `InternalWONumber`, `LifecycleStatus+ScheduledDate`
|
||||
- Min 2 caracteres em `search` para evitar full-table scan
|
||||
|
||||
---
|
||||
|
||||
## Próximos passos (gate staging)
|
||||
|
||||
1. Executar `scripts/volume-discovery.ps1` contra staging read-only
|
||||
2. Preencher tabela §3 do volume report
|
||||
3. Atualizar este documento com tier real
|
||||
4. Repetir load test k6 em staging; ajustar `pageSize` max se tier = M
|
||||
|
||||
---
|
||||
|
||||
## Referências
|
||||
|
||||
- [volume-discovery-report.md](../phase-0/volume-discovery-report.md)
|
||||
- [phase-4/README.md](./README.md)
|
||||
- Roadmap §10, §12 Fase 4
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
# Fase 5 — Scheduled Domain Events
|
||||
|
||||
**Programa:** Work Orders Board
|
||||
**Objetivo:** Jobs agendados in-process (`IHostedService`) para `WeekRolled` → `CarriedOver++` com idempotência WO+semana, audit System, cache opcional de `PastDue`, e garantia de que leituras do board continuam on-read como fonte da verdade.
|
||||
|
||||
**Depende de:** [Fase 0](../phase-0/README.md), [Fase 1](../phase-1/README.md), [Fase 2](../phase-2/README.md), [Fase 3](../phase-3/README.md), [Fase 4](../phase-4/README.md)
|
||||
|
||||
---
|
||||
|
||||
## Regra de elegibilidade WeekRolled
|
||||
|
||||
```text
|
||||
sourceWeek = semana operacional encerrada (segunda a sexta UTC)
|
||||
Elegível se:
|
||||
- ScheduledDate.Date ∈ [sourceWeekStart, sourceWeekEnd]
|
||||
- LifecycleStatus ∉ {Complete, Canceled, Closed}
|
||||
- istemplate != true (ou null)
|
||||
- Ainda não processado em WorkOrderWeekRolledLedger (WorkOrderId + SourceWeekStart)
|
||||
```
|
||||
|
||||
**Gate PO (pendente):** confirmar se WOs só com `TargetWeek` (sem `ScheduledDate`) entram no carry-over. A implementação atual considera **apenas** WOs com `ScheduledDate` na janela.
|
||||
|
||||
---
|
||||
|
||||
## Idempotência
|
||||
|
||||
Chave composta `(WorkOrderId, SourceWeekStart)` na tabela `WorkOrderWeekRolledLedger`. Re-execução do job ou endpoint admin na mesma semana não duplica `CarriedOver`.
|
||||
|
||||
---
|
||||
|
||||
## Contrato de audit
|
||||
|
||||
Um evento `WeekRolled` por WO processado:
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| `EventType` | `System` |
|
||||
| `ActorType` | `System` |
|
||||
| `Action` | `WeekRolled` |
|
||||
| `FieldName` | `CarriedOver` |
|
||||
| `OldValue` / `NewValue` | numéricos (string) |
|
||||
| `CorrelationId` | `week:{yyyy-MM-dd}` (segunda da semana fonte) |
|
||||
|
||||
Não aciona `ManualEditFlag` nem field locks.
|
||||
|
||||
---
|
||||
|
||||
## Regra de ouro — isPastDue
|
||||
|
||||
Jobs **nunca** são fonte da verdade para `isPastDue`. O board continua calculando via `WorkOrderDerivedFields.IsPastDue` on-read. O cache `OperationalFlags.PastDue` é otimização opcional (`WorkOrderJobs:PastDueCache:Enabled`, default **false**).
|
||||
|
||||
---
|
||||
|
||||
## Configuração
|
||||
|
||||
```json
|
||||
"WorkOrderJobs": {
|
||||
"WeekRolled": { "Enabled": true, "RunAtUtc": "00:05", "DayOfWeek": "Monday" },
|
||||
"PastDueCache": { "Enabled": false, "RunAtUtc": "00:10" }
|
||||
}
|
||||
```
|
||||
|
||||
Variáveis de ambiente (`.env.example`):
|
||||
|
||||
```text
|
||||
WorkOrderJobs__WeekRolled__Enabled=true
|
||||
WorkOrderJobs__PastDueCache__Enabled=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoints admin (ops-only)
|
||||
|
||||
| Endpoint | Auth | Descrição |
|
||||
|----------|------|-----------|
|
||||
| `POST /api/workorders/jobs/week-rolled?sourceWeekStart=2026-06-16` | Admin | Reprocessa semana (segunda-feira). Idempotente. |
|
||||
| `POST /api/workorders/jobs/past-due-cache` | Admin | Atualiza cache `OperationalFlags.PastDue` |
|
||||
|
||||
---
|
||||
|
||||
## Código entregue
|
||||
|
||||
| Camada | Arquivo |
|
||||
|--------|---------|
|
||||
| Ledger | `Data.SeaHavenIndustries/Models/WorkOrderWeekRolledLedger.cs` |
|
||||
| Migration | `20260624210000_Phase5_DomainEvents.cs` |
|
||||
| Data | `SeaHaven.DataServices/Implementation/WorkOrderDomainJobDataService.cs` |
|
||||
| WeekRolled | `SeaHaven.Services/Implementation/WorkOrderWeekRolledService.cs` |
|
||||
| PastDue cache | `SeaHaven.Services/Implementation/PastDueCacheService.cs` |
|
||||
| Hosted | `Api.SeaHavenIndustries/HostedServices/*.cs` |
|
||||
| API | `WorkOrderJobsController` |
|
||||
| Testes | `SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs` |
|
||||
|
||||
---
|
||||
|
||||
## Monitoramento
|
||||
|
||||
Logs estruturados com `CorrelationId` da semana. Ao concluir: `processed`, `skipped`, `failed`, `durationMs`.
|
||||
|
||||
**Alerta ops:** se `failed > 0` ou job não executou em 8 dias → investigar + `POST /jobs/week-rolled` manual.
|
||||
|
||||
---
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [x] Job semanal incrementa `carriedOver` para WOs elegíveis da semana anterior
|
||||
- [x] Re-execução na mesma semana não duplica (ledger WO+semana)
|
||||
- [x] Audit `WeekRolled` System com old/new `CarriedOver` e `CorrelationId`
|
||||
- [x] `GET /api/workorders/board` continua com `isPastDue` derivado on-read
|
||||
- [x] Endpoint admin permite reprocessar semana específica
|
||||
- [x] Testes cobrem idempotência, terminal skip, audit e on-read correctness
|
||||
|
||||
---
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Message queue / worker externo
|
||||
- Alteração do contrato REST do board para usar `OperationalFlags`
|
||||
- WOs week-only (`TargetWeek` sem `ScheduledDate`) — gate PO
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
# Fase 6 — Completion Doc e Slide-over
|
||||
|
||||
**Programa:** Work Orders Board
|
||||
**Objetivo:** Contrato SHOC para slide-over (`WOSlideOver`): detalhe unificado, `docStatus` WO-level com templates por serviço, e tabs Comments/Audit/Media alinhadas ao frontend.
|
||||
|
||||
**Depende de:** [Fase 0](../phase-0/README.md) … [Fase 5](../phase-5/README.md)
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### `GET /api/workorders/{id}/detail`
|
||||
|
||||
Payload único para abrir o slide-over.
|
||||
|
||||
**Autenticação:** Bearer JWT (`[Authorize]`)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"info": { /* WorkOrderDetailInfoDto — board row + description/trade/original* */ },
|
||||
"completion": {
|
||||
"docStatus": "No",
|
||||
"template": { "id": 1, "name": "HVAC PM Completion", "serviceKey": "HVAC PM", "templateUrl": "..." },
|
||||
"signOffName": null,
|
||||
"signOffAttachment": null,
|
||||
"signOffSignature": null,
|
||||
"dispatchSignoffs": []
|
||||
},
|
||||
"comments": [{ "id": 1, "authorId": "guid", "text": "...", "time": "2026-06-01T12:00:00.0000000Z" }],
|
||||
"audit": [{ "type": "manual", "dispatcherId": "guid", "action": "FieldChanged", "fieldName": "DocStatus", "oldValue": "No", "newValue": "Yes", "time": "..." }],
|
||||
"media": [{ "id": 5, "category": "Extra", "url": "...", "isLegacy": false }]
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <token>" \
|
||||
"https://localhost:5001/api/workorders/42/detail"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/workorders/{id}/audit?limit=50`
|
||||
|
||||
Audit tab lazy refresh. Schema FE: `{ type, dispatcherId?, action, fieldName?, oldValue, newValue, time, dispatchId? }`.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/workorders/{id}/comments`
|
||||
|
||||
### `POST /api/workorders/{id}/comments`
|
||||
|
||||
```json
|
||||
{ "text": "Called vendor" }
|
||||
```
|
||||
|
||||
Response: `{ id, authorId, text, time, documents? }`.
|
||||
|
||||
---
|
||||
|
||||
### Completion templates
|
||||
|
||||
| Método | Rota | Auth | Descrição |
|
||||
|--------|------|------|-----------|
|
||||
| GET | `/api/workorders/completion-templates` | JWT | Query `serviceKey`, `workOrderType` |
|
||||
| GET | `/api/workorders/completion-templates/{id}` | JWT | Detalhe |
|
||||
| POST | `/api/workorders/completion-templates` | Admin | Criar template |
|
||||
| PUT | `/api/workorders/completion-templates/{id}` | Admin | Atualizar |
|
||||
| DELETE | `/api/workorders/completion-templates/{id}` | Admin | Soft delete |
|
||||
|
||||
Lookup na WO: `Trade` → fallback `WorkOrderType`.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/workorders/{id}/completion-doc`
|
||||
|
||||
Upload PDF preenchido (multipart).
|
||||
|
||||
| Campo form | Obrigatório |
|
||||
|------------|-------------|
|
||||
| `file` | Sim |
|
||||
| `signOffName` | Não |
|
||||
| `signOffSignature` | Não |
|
||||
| `workOrderVersion` | Recomendado (Base64 RowVersion) |
|
||||
|
||||
**Regras:**
|
||||
- WO read-only (`Canceled`/`Complete`/`Closed`) → 422
|
||||
- Sucesso → `SignOffAttachment` + `DocStatus=Yes` + audit `FieldChanged`
|
||||
- Dispatch signoffs → somente leitura no slide-over
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Authorization: Bearer <token>" \
|
||||
-F "file=@completion.pdf" \
|
||||
-F "signOffName=Jane Doe" \
|
||||
"https://localhost:5001/api/workorders/42/completion-doc"
|
||||
```
|
||||
|
||||
**Inline toggle:** `PATCH /api/workorders/{id}/board` com `field=docStatus` (Fase 2).
|
||||
|
||||
---
|
||||
|
||||
### Media
|
||||
|
||||
| Método | Rota | Descrição |
|
||||
|--------|------|-----------|
|
||||
| GET | `/api/workorders/{id}/media` | Lista unificada com `category` |
|
||||
| POST | `/api/workorders/{id}/media` | multipart: `category` (Before/After/Extra/Completion) + `file` |
|
||||
| DELETE | `/api/workorders/{id}/media/{mediaId}` | Soft delete (somente attachments com `id > 0`) |
|
||||
|
||||
Legacy columns (`BeforPhotoAttachment`, `AfterPhotoAttachment`, `SignOffAttachment`) aparecem na projeção com `isLegacy: true`.
|
||||
|
||||
---
|
||||
|
||||
## Schema
|
||||
|
||||
Migration `Phase6_CompletionSlideOver`:
|
||||
|
||||
- Tabela `CompletionDocTemplates`
|
||||
- Coluna `Category` em `workOrderAttachments`
|
||||
- Backfill SQL: `SignOffAttachment` preenchido → `DocStatus=Yes`
|
||||
|
||||
Script ops: [`scripts/backfill-docstatus.ps1`](../../scripts/backfill-docstatus.ps1)
|
||||
|
||||
---
|
||||
|
||||
## Código entregue
|
||||
|
||||
| Camada | Arquivo |
|
||||
|--------|---------|
|
||||
| Model | `CompletionDocTemplate.cs`, `WorkOrderMediaCategory` enum |
|
||||
| Migration | `20260625120000_Phase6_CompletionSlideOver.cs` |
|
||||
| Data | `WorkOrderDetailDataService`, `CompletionDocTemplateDataService` |
|
||||
| Services | `WorkOrderDetailService`, `WorkOrderCommentService`, `WorkOrderCompletionService`, `WorkOrderMediaService` |
|
||||
| DTOs | `WorkOrderDetailDTOs.cs` |
|
||||
| Helpers | `WorkOrderAuditProjection`, `WorkOrderCommentProjection`, `WorkOrderMediaProjection` |
|
||||
| API | `WorkOrderController` — rotas `/detail`, `/comments`, `/audit`, `/completion-doc`, `/media`, `/completion-templates` |
|
||||
| Testes | `WorkOrderPhase6Tests.cs` |
|
||||
|
||||
---
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [x] `GET /detail` retorna Info + Completion + Comments + Audit + Media no contrato FE
|
||||
- [x] Coluna COMP DOC do board via `docStatus` no `WorkOrderBoardRowDto` (Fase 1)
|
||||
- [x] `CompletionDocTemplate` CRUD admin + lookup por `serviceKey`/`workOrderType`
|
||||
- [x] `POST completion-doc` persiste attachment, define `DocStatus=Yes`, audit
|
||||
- [x] Comments/Audit tabs sem adapter no FE
|
||||
- [x] Media com `category`; legado mapeado
|
||||
- [x] Endpoints legados (`GetWorkorderById`, `GetCommentsByWorkorderId`) inalterados
|
||||
- [x] Testes: DocStatus PATCH, detail, completion upload, comments, media
|
||||
|
||||
---
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Geração de PDF server-side
|
||||
- Alteração Vendor Portal checklist/signoff
|
||||
- Rollout / sunset Blazor (Fase 7)
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
# Fase 7 — Rollout e Produção
|
||||
|
||||
**Programa:** Work Orders Board
|
||||
**Objetivo:** Piloto SHOC, rollout gradual, monitoramento de coexistência, sunset Blazor WO, cutover Lambda→API e deprecação legado.
|
||||
|
||||
**Depende de:** [Fase 0](../phase-0/README.md) … [Fase 6](../phase-6/README.md) concluídas e validadas em staging.
|
||||
|
||||
---
|
||||
|
||||
## Gates
|
||||
|
||||
- **Entrada:** [phase-7-gates-signoff.md](phase-7-gates-signoff.md)
|
||||
- **Saída:** piloto UAT, 100% SHOC, zero P1 × 2 semanas, Blazor sunset, Lambda cutover, ops health ativo
|
||||
|
||||
---
|
||||
|
||||
## Runbook de rollout (ordem obrigatória)
|
||||
|
||||
1. Fechar gates de entrada Fase 7
|
||||
2. Deploy API com ops health + ingest (ingest desabilitado em prod inicialmente)
|
||||
3. SHOC: feature flag board em staging → piloto 1–2 dispatchers em prod
|
||||
4. UAT dispatchers ([roteiros abaixo](#uat-dispatchers))
|
||||
5. Rollout gradual SHOC até 100%
|
||||
6. Habilitar dual-run Lambda (`ingest` + Dynamo)
|
||||
7. Validar drift → cutover Lambda só API
|
||||
8. `Sync:Enabled=false`
|
||||
9. `BlazorWorkOrderSunset:Enabled=true`
|
||||
10. `LegacyEndpoints:DeprecationEnabled=true` + data Sunset
|
||||
11. Remover Sync/legado após período de aviso (30 dias)
|
||||
|
||||
---
|
||||
|
||||
## Endpoints Fase 7
|
||||
|
||||
### `GET /api/workorders/ops/health`
|
||||
|
||||
Saúde operacional para coexistência multi-consumidor.
|
||||
|
||||
**Auth:** Admin JWT
|
||||
|
||||
```json
|
||||
{
|
||||
"lastWeekRolledRunUtc": "2026-06-23T00:05:12Z",
|
||||
"lastPastDueCacheRunUtc": null,
|
||||
"lastWeekRolledError": null,
|
||||
"lastPastDueCacheError": null,
|
||||
"syncRejectedLast24h": 3,
|
||||
"fieldLockCount": 142,
|
||||
"syncEnabled": true,
|
||||
"ingestEnabled": true,
|
||||
"legacyDeprecationEnabled": false,
|
||||
"legacySunsetDate": null,
|
||||
"checkedAtUtc": "2026-06-25T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
Ver [cloudwatch-alerts.md](cloudwatch-alerts.md).
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/workorders/ingest`
|
||||
|
||||
Ingest direto idempotente (substitui ponte DynamoDB para WOs).
|
||||
|
||||
**Auth:** header `X-Ingest-Key` (config `WorkOrderIngest:ApiKey`)
|
||||
|
||||
**Body (single ou batch):**
|
||||
|
||||
```json
|
||||
{
|
||||
"externalWorkOrderId": "ext-wo-12345",
|
||||
"description": "HVAC unit not cooling",
|
||||
"woStatus": "new",
|
||||
"severity": "2",
|
||||
"customer": "Acme Corp",
|
||||
"siteCode": "BK5",
|
||||
"building": "Building A",
|
||||
"address": "123 Main St",
|
||||
"dueDate": "2026-07-01T00:00:00Z",
|
||||
"dateReported": "2026-06-20T10:00:00Z",
|
||||
"scheduledStart": null,
|
||||
"sourceEmailS3Key": null,
|
||||
"createdAt": "2026-06-20T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Batch:**
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [ { "externalWorkOrderId": "...", "description": "..." } ]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"created": 1,
|
||||
"updated": 0,
|
||||
"results": [{ "externalWorkOrderId": "ext-wo-12345", "workOrderId": 42, "created": true }]
|
||||
}
|
||||
```
|
||||
|
||||
Mapeamento completo: [lambda-ingest-discovery.md](lambda-ingest-discovery.md)
|
||||
|
||||
---
|
||||
|
||||
## Matriz de endpoints
|
||||
|
||||
| Tipo | Exemplos | Política Fase 7 |
|
||||
|------|----------|-----------------|
|
||||
| Board (SHOC) | `GET board`, `PATCH {id}/board`, `POST board` | **Produção** |
|
||||
| Slide-over | `GET {id}/detail`, completion-doc, media | **Produção** |
|
||||
| Ingest | `POST ingest` | Dual-run → produção |
|
||||
| Sync | `POST api/Sync/WorkOrders` | Desligar após cutover (`Sync:Enabled`) |
|
||||
| Legado | `GetWorkOrderList`, `AddWorkorder`, `ChangeStatus` | Deprecation headers |
|
||||
| Jobs | `POST jobs/week-rolled` | Ops Admin |
|
||||
|
||||
---
|
||||
|
||||
## Configuração
|
||||
|
||||
### API (`Api.SeaHavenIndustries`)
|
||||
|
||||
```json
|
||||
{
|
||||
"FrontendBaseUrl": "https://shoc.seahaven.com",
|
||||
"WorkOrderIngest": {
|
||||
"Enabled": true,
|
||||
"ApiKey": "${WORKORDER_INGEST_API_KEY}"
|
||||
},
|
||||
"Sync": {
|
||||
"Enabled": true
|
||||
},
|
||||
"LegacyEndpoints": {
|
||||
"DeprecationEnabled": false,
|
||||
"SunsetDate": "2026-12-31"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Env vars: ver [.env.example](../../../.env.example)
|
||||
|
||||
### Blazor (`SeaHavenIndustries`)
|
||||
|
||||
```json
|
||||
{
|
||||
"BlazorWorkOrderSunset": {
|
||||
"Enabled": false,
|
||||
"ShocBaseUrl": "https://shoc.seahaven.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Quando `Enabled=true`: nav Work Orders aponta para SHOC; mutações EF bloqueadas; banner nas páginas WO.
|
||||
|
||||
---
|
||||
|
||||
## UAT dispatchers
|
||||
|
||||
Roteiro manual — marcar cada item antes de expandir rollout.
|
||||
|
||||
| # | Cenário | Pass |
|
||||
|---|---------|------|
|
||||
| 1 | Login SHOC; carregar semana (Mon–Fri + Unscheduled) | [ ] |
|
||||
| 2 | Filtros: dispatcher, My WOs, tipo, busca contextual | [ ] |
|
||||
| 3 | Inline edit célula (status, assignee, due) | [ ] |
|
||||
| 4 | Conflito 409 — outro usuário editou; mensagem clara | [ ] |
|
||||
| 5 | Criar WO wizard (Incomplete → Scheduled auto) | [ ] |
|
||||
| 6 | Cancel soft; campos read-only pós-cancel | [ ] |
|
||||
| 7 | Advanced search cross-week | [ ] |
|
||||
| 8 | Slide-over: detail, comments, audit, media | [ ] |
|
||||
| 9 | Upload completion doc; docStatus no board | [ ] |
|
||||
| 10 | Badge PastDue / CarriedOver após segunda-feira | [ ] |
|
||||
|
||||
**Sign-off UAT**
|
||||
|
||||
| Dispatcher | Data | PO |
|
||||
|------------|------|-----|
|
||||
| | | |
|
||||
|
||||
---
|
||||
|
||||
## Piloto e rollout SHOC
|
||||
|
||||
Controle **somente no frontend** (feature flags). Backend não filtra dispatchers.
|
||||
|
||||
Critérios para avançar:
|
||||
|
||||
- Zero P1 na semana do piloto
|
||||
- `GET board` latência aceitável (Tier S)
|
||||
- Vendor Portal regression verde
|
||||
|
||||
---
|
||||
|
||||
## Sunset Blazor
|
||||
|
||||
Ver [data-ownership-model.md](../phase-0/data-ownership-model.md) §3.
|
||||
|
||||
1. Congelar `WorkorderService` (bugfix only) até data PO
|
||||
2. Habilitar `BlazorWorkOrderSunset:Enabled`
|
||||
3. Smoke final [smoke-checklist.md](../phase-0/smoke-checklist.md) Blazor EF
|
||||
|
||||
---
|
||||
|
||||
## Cutover Lambda
|
||||
|
||||
Ver [lambda-ingest-discovery.md](lambda-ingest-discovery.md).
|
||||
|
||||
---
|
||||
|
||||
## Testes automatizados
|
||||
|
||||
```powershell
|
||||
dotnet test SeaHavenIndustries.Tests --filter "FullyQualifiedName~WorkOrderPhase7"
|
||||
```
|
||||
|
||||
Suite: `WorkOrderPhase7CoexistenceTests.cs`
|
||||
|
||||
---
|
||||
|
||||
## Critérios de aceite Fase 7
|
||||
|
||||
- [x] `GET /api/workorders/ops/health`
|
||||
- [x] `POST /api/workorders/ingest` com API key
|
||||
- [x] `Sync:Enabled` feature flag
|
||||
- [x] Legacy deprecation middleware
|
||||
- [x] Blazor sunset config + guard mutações
|
||||
- [x] Documentação gates, UAT, Lambda mapping
|
||||
- [ ] Piloto prod (ops/PO)
|
||||
- [ ] 100% dispatchers SHOC (ops/PO)
|
||||
- [ ] Zero P1 × 2 semanas (ops/PO)
|
||||
- [ ] Lambda cutover executado (ops)
|
||||
|
||||
---
|
||||
|
||||
## Comunicação
|
||||
|
||||
| Audiência | Mensagem | Quando |
|
||||
|-----------|----------|--------|
|
||||
| Dispatchers piloto | Novo board SHOC; suporte dedicado | Início piloto |
|
||||
| Todos dispatchers | Rollout gradual; treinamento | Durante rollout |
|
||||
| Usuários Blazor | WO migrou para SHOC; link direto | Sunset Blazor |
|
||||
| Engenharia | Sync desligado; usar ingest API | Pós-cutover |
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# Alertas Operacionais — Fase 7
|
||||
|
||||
**Endpoint de saúde:** `GET /api/workorders/ops/health` (Admin JWT)
|
||||
|
||||
---
|
||||
|
||||
## Métricas expostas
|
||||
|
||||
| Campo | Fonte | Uso |
|
||||
|-------|-------|-----|
|
||||
| `lastWeekRolledRunUtc` | `WorkOrderJobRunState` + ledger | Job segunda-feira |
|
||||
| `lastPastDueCacheRunUtc` | `WorkOrderJobRunState` | Cache opcional PastDue |
|
||||
| `syncRejectedLast24h` | `WorkOrderAuditLogs` Action=SyncRejected | Conflito SHOC vs ingest |
|
||||
| `fieldLockCount` | `WorkOrderFieldLocks` | Adoção edição manual |
|
||||
| `syncEnabled` | `Sync:Enabled` | Estado ponte Dynamo |
|
||||
| `ingestEnabled` | `WorkOrderIngest:Enabled` | Ingest direto ativo |
|
||||
|
||||
---
|
||||
|
||||
## Alertas recomendados (Elastic Beanstalk / CloudWatch)
|
||||
|
||||
| Alerta | Condição | Severidade |
|
||||
|--------|----------|------------|
|
||||
| WeekRolled stale | `lastWeekRolledRunUtc` > 8 dias | P1 |
|
||||
| WeekRolled job error | `lastWeekRolledError` não nulo | P1 |
|
||||
| SyncRejected spike | `syncRejectedLast24h` > 50 | P2 |
|
||||
| API 5xx board | ALB/Beanstalk 5xx rate > 1% em `/api/workorders/board` | P1 |
|
||||
| Ingest auth failures | 401 em `/api/workorders/ingest` > 10/h | P2 |
|
||||
| Sync disabled em prod sem cutover | `syncEnabled=false` e ingest não validado | P2 |
|
||||
|
||||
---
|
||||
|
||||
## Verificação manual (ops)
|
||||
|
||||
```powershell
|
||||
# Com token Admin
|
||||
curl -H "Authorization: Bearer <token>" https://<api-host>/api/workorders/ops/health
|
||||
```
|
||||
|
||||
Agendar checagem diária durante piloto e rollout.
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
# Lambda Ingest — Descoberta e Mapeamento de Campos
|
||||
|
||||
**Fase:** 7
|
||||
**Status:** Documentação de cutover (código Lambda fora deste repositório)
|
||||
|
||||
---
|
||||
|
||||
## 1. Localização da Lambda
|
||||
|
||||
| Item | Valor |
|
||||
|------|-------|
|
||||
| Nome referenciado | `workorder-ingest` |
|
||||
| Referência código | [TODO.md](../../../TODO.md) L50–51 |
|
||||
| Fluxo atual | Lambda → DynamoDB (`WorkOrders`) → `POST api/Sync/WorkOrders` |
|
||||
| Fluxo alvo | Lambda → `POST api/workorders/ingest` (API key) |
|
||||
|
||||
**Ação pendente ops:** localizar repositório/infra AWS (SAM, Terraform, console Lambda) e preencher owner na [phase-7-gates-signoff.md](phase-7-gates-signoff.md).
|
||||
|
||||
---
|
||||
|
||||
## 2. Tabelas DynamoDB (SyncController)
|
||||
|
||||
| Tabela | Endpoint sync | Uso |
|
||||
|--------|---------------|-----|
|
||||
| `WorkOrders` | `POST api/Sync/WorkOrders` | Upsert WO por `work_order_id` |
|
||||
| `WorkOrderComments` | `POST api/Sync/Comments` | Comentários cliente |
|
||||
| `VendorReplies` | `POST api/Sync/VendorReplies` | Respostas vendor |
|
||||
|
||||
Cutover Fase 7 foca em **WorkOrders**; comments/replies permanecem no Sync até migração separada.
|
||||
|
||||
---
|
||||
|
||||
## 3. Mapeamento DynamoDB → API ingest
|
||||
|
||||
Campos lidos em [SyncController.cs](../../../Api.SeaHavenIndustries/Controllers/SyncController.cs) e espelhados em `WorkOrderIngestPayload`:
|
||||
|
||||
| Campo Dynamo | Campo API ingest | Campo SQL | Merge policy (update) |
|
||||
|--------------|------------------|-----------|------------------------|
|
||||
| `work_order_id` | `externalWorkOrderId` | `ExternalWorkOrderId` | Chave idempotente |
|
||||
| `description` | `description` | `Description`, `WorkerOrderTitle` | Sim se não locked |
|
||||
| `wo_status` | `woStatus` | `Status` (mapeado) | Sim se não locked |
|
||||
| `severity` | `severity` | `Priority`, `Severity` | Sim se não locked |
|
||||
| `customer` | `customer` | `Customer` | Direto na criação |
|
||||
| `site_code` | `siteCode` | `SiteCode` | Sim se não locked |
|
||||
| `building` | `building` | `Building` | Sim se não locked |
|
||||
| `address` | `address` | `Locations` (resolve/create) | LocationId |
|
||||
| `due_date` | `dueDate` | `DueDate` | Sim se não locked |
|
||||
| `date_reported` | `dateReported` | `DateReported` | Direto |
|
||||
| `scheduled_start` | `scheduledStart` | `ScheduledStart` | Direto |
|
||||
| `source_email_s3_key` | `sourceEmailS3Key` | `SourceEmailS3Key` | Direto |
|
||||
| `created_at` | `createdAt` | `CreatedDate` | Criação only |
|
||||
|
||||
### Mapeamento `wo_status` → SQL `Status`
|
||||
|
||||
| Dynamo | SQL |
|
||||
|--------|-----|
|
||||
| `new`, `assigned`, `unknown` | `Open` |
|
||||
| `in_progress` | `In Progress` |
|
||||
| `on_hold` | `On Hold` |
|
||||
| `completed` | `Done` |
|
||||
| `cancelled` | `Cancelled` |
|
||||
|
||||
### Mapeamento `severity` → `Priority`
|
||||
|
||||
`Sev {severity}` (ex.: `3` → `Sev 3`)
|
||||
|
||||
---
|
||||
|
||||
## 4. Auth serviço-a-serviço
|
||||
|
||||
| Header | Config |
|
||||
|--------|--------|
|
||||
| `X-Ingest-Key` | `WorkOrderIngest:ApiKey` (env `WorkOrderIngest__ApiKey`) |
|
||||
|
||||
Lambda deve enviar o header em cada `POST /api/workorders/ingest`. Não usar JWT de usuário dispatcher.
|
||||
|
||||
---
|
||||
|
||||
## 5. Estratégia dual-run
|
||||
|
||||
1. **Semana 1–2:** Lambda grava DynamoDB **e** chama API ingest
|
||||
2. **Validação:** `GET /api/workorders/ops/health` + script amostra `ExternalWorkOrderId`
|
||||
3. **Cutover:** Lambda só API; `Sync:Enabled=false`
|
||||
4. **Retire:** backup DynamoDB → desativar tabelas
|
||||
|
||||
---
|
||||
|
||||
## 6. Critérios de cutover
|
||||
|
||||
- [ ] `POST /api/workorders/board` estável (criação SHOC)
|
||||
- [ ] Auth ingest testada em staging
|
||||
- [ ] Zero drift em amostra de 100 WOs
|
||||
- [ ] Owner Lambda assinou runbook
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# Phase 7 — Gates Sign-Off Checklist
|
||||
|
||||
**Programa:** Work Orders Board
|
||||
**Fase:** 7 — Rollout e Produção
|
||||
|
||||
---
|
||||
|
||||
## Gate de entrada (GO Fase 7)
|
||||
|
||||
Bloqueia piloto em produção até todos os itens estarem verdes.
|
||||
|
||||
| # | Gate | Artefato / evidência | Status |
|
||||
|---|------|----------------------|--------|
|
||||
| 1 | Fases 0–6 validadas em staging | `docs/work-orders/phase-0` … `phase-6` READMEs | [ ] |
|
||||
| 2 | Sign-off CTO/PO Fase 0 | [phase-0-gates-signoff.md](../phase-0/phase-0-gates-signoff.md) | [ ] |
|
||||
| 3 | Dry-run migrations Phase0–Phase6 | [migration-dry-run-report.md](../phase-0/migration-dry-run-report.md) | [ ] |
|
||||
| 4 | Smoke multi-consumidor | [smoke-checklist.md](../phase-0/smoke-checklist.md) | [ ] |
|
||||
| 5 | Vendor Portal regression | Checklist dispatch checklist/signoff | [ ] |
|
||||
| 6 | SHOC E2E staging | Contratos phase-1 … phase-6 integrados | [ ] |
|
||||
| 7 | Tier volume confirmado | [volume-discovery-report.md](../phase-0/volume-discovery-report.md) | [ ] |
|
||||
|
||||
### Checklist PO ([consumer-inventory.md](../phase-0/consumer-inventory.md))
|
||||
|
||||
- [ ] App mobile externo confirmado (sim/não/N/A)
|
||||
- [ ] Data sunset Blazor WO definida: _______________
|
||||
- [ ] `FrontendBaseUrl` produção → SHOC configurado
|
||||
- [ ] Owner Lambda cutover nomeado: _______________
|
||||
|
||||
**Assinatura GO Fase 7**
|
||||
|
||||
| Papel | Nome | Data |
|
||||
|-------|------|------|
|
||||
| CTO | | |
|
||||
| PO | | |
|
||||
|
||||
---
|
||||
|
||||
## Gates de saída (DONE Fase 7)
|
||||
|
||||
| Gate | Critério |
|
||||
|------|----------|
|
||||
| Piloto | 1–2 dispatchers SHOC em prod; UAT assinado |
|
||||
| Rollout | 100% dispatchers no board SHOC |
|
||||
| Estabilidade | Zero P1 por 2 semanas consecutivas |
|
||||
| Blazor | Módulo WO sunset (`BlazorWorkOrderSunset:Enabled`) |
|
||||
| Lambda | Ingest direto `POST /api/workorders/ingest`; Dynamo/Sync desligados |
|
||||
| Monitoramento | `GET /api/workorders/ops/health` + alertas documentados |
|
||||
| Legado | Headers `Sunset`/`Deprecation` nos endpoints legado |
|
||||
|
||||
**Assinatura DONE Fase 7**
|
||||
|
||||
| Papel | Nome | Data |
|
||||
|-------|------|------|
|
||||
| CTO | | |
|
||||
| PO | | |
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Backfill DocStatus from legacy SignOffAttachment (idempotent)
|
||||
# Run against target SQL Server after Phase6_CompletionSlideOver migration.
|
||||
|
||||
param(
|
||||
[string]$ConnectionString = $env:ConnectionStrings__DefaultConnection
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ConnectionString)) {
|
||||
Write-Error "Set ConnectionStrings__DefaultConnection or pass -ConnectionString"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$sql = @"
|
||||
UPDATE workOrders
|
||||
SET DocStatus = 1
|
||||
WHERE DocStatus IS NULL
|
||||
AND SignOffAttachment IS NOT NULL
|
||||
AND LTRIM(RTRIM(SignOffAttachment)) <> '';
|
||||
"@
|
||||
|
||||
Write-Host "Backfilling DocStatus=Yes where SignOffAttachment exists..."
|
||||
Invoke-Sqlcmd -ConnectionString $ConnectionString -Query $sql
|
||||
Write-Host "Done."
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
# Seed sintético de Work Orders para load test Tier S
|
||||
#
|
||||
# Uso:
|
||||
# $env:SQL_CONNECTION_STRING = "Server=localhost;Database=SeaHaven;..."
|
||||
# .\scripts\seed-work-orders-search.ps1 -Count 15000
|
||||
|
||||
param(
|
||||
[int]$Count = 5000,
|
||||
[string]$ConnectionString = $env:SQL_CONNECTION_STRING
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ConnectionString)) {
|
||||
Write-Error "Defina SQL_CONNECTION_STRING ou passe -ConnectionString."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$sites = @("BK5", "BK6", "BK7", "BK8", "BK9", "LA1", "LA2", "NY1")
|
||||
$trades = @("HVAC PM", "Plumbing PM", "Electrical PM", "Roof Inspection", "Fire Safety")
|
||||
$baseDate = Get-Date "2026-01-06"
|
||||
|
||||
Write-Host "Inserindo $Count WOs sintéticas..." -ForegroundColor Cyan
|
||||
|
||||
$batchSize = 500
|
||||
$inserted = 0
|
||||
|
||||
while ($inserted -lt $Count) {
|
||||
$batch = [Math]::Min($batchSize, $Count - $inserted)
|
||||
$values = @()
|
||||
|
||||
for ($i = 0; $i -lt $batch; $i++) {
|
||||
$id = $inserted + $i + 1
|
||||
$site = $sites[$id % $sites.Length]
|
||||
$trade = $trades[$id % $trades.Length]
|
||||
$weekOffset = $id % 52
|
||||
$scheduled = $baseDate.AddDays($weekOffset * 7 + ($id % 5))
|
||||
$woNumber = ("1{0:D10}" -f $id)
|
||||
|
||||
$values += "('$woNumber', '$site', '$trade', '$($scheduled.ToString('yyyy-MM-dd'))', 2, 0)"
|
||||
}
|
||||
|
||||
$sql = @"
|
||||
INSERT INTO workOrders (InternalWONumber, SiteCode, Trade, ScheduledDate, LifecycleStatus, istemplate, RescheduleCount, CarriedOver, CreatedDate)
|
||||
VALUES $($values -join ',');
|
||||
"@
|
||||
|
||||
sqlcmd -Q $sql -b
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Falha no batch $inserted"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$inserted += $batch
|
||||
Write-Host " $inserted / $Count"
|
||||
}
|
||||
|
||||
Write-Host "Seed concluído." -ForegroundColor Green
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
-- Spike Status Mapping (G2): seed legado + enum compacto + queries de análise
|
||||
-- Executar APÓS dotnet ef database update
|
||||
-- Uso: sqlcmd -S localhost,1433 -U sa -P "SeaHaven_Dev_2026!" -d SeahavenIndustries -C -i scripts/spike-status-mapping.sql
|
||||
|
||||
SET NOCOUNT ON;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Seed status spike (prefixo SPIKE-STATUS-)
|
||||
-- Cobre formatos legado (com espaço) e canônico API (enum ToString)
|
||||
-- ---------------------------------------------------------------------------
|
||||
IF NOT EXISTS (SELECT 1 FROM workOrders WHERE InternalWONumber LIKE 'SPIKE-STATUS-%')
|
||||
BEGIN
|
||||
DECLARE @weekStart date = CAST(GETDATE() AS date);
|
||||
|
||||
INSERT INTO workOrders (
|
||||
InternalWONumber, WorkerOrderTitle, SiteCode, Status, Priority,
|
||||
ScheduledDate, DueDate, Trade, Problem, AssignTo, istemplate, CreatedDate
|
||||
)
|
||||
VALUES
|
||||
-- Legado Blazor / Sync (com espaço)
|
||||
('SPIKE-STATUS-001', 'Status spike - Open legado', 'SS1', 'Open', 'Medium', DATEADD(day, 0, @weekStart), DATEADD(day, 5, @weekStart), 'HVAC', 'Repair', NULL, 0, GETDATE()),
|
||||
('SPIKE-STATUS-002', 'Status spike - In Progress legado','SS2', 'In Progress', 'Medium', DATEADD(day, 1, @weekStart), DATEADD(day, 6, @weekStart), 'Plumbing', 'Repair', NULL, 0, GETDATE()),
|
||||
('SPIKE-STATUS-003', 'Status spike - On Hold legado', 'SS3', 'On Hold', 'Low', DATEADD(day, 2, @weekStart), DATEADD(day, 10, @weekStart), 'Electrical', 'Hold', NULL, 0, GETDATE()),
|
||||
('SPIKE-STATUS-004', 'Status spike - Done legado', 'SS4', 'Done', 'Low', DATEADD(day, 3, @weekStart), DATEADD(day, 4, @weekStart), 'General', 'Complete', NULL, 0, GETDATE()),
|
||||
('SPIKE-STATUS-005', 'Status spike - Cancelled legado', 'SS5', 'Cancelled', 'Low', DATEADD(day, 4, @weekStart), DATEADD(day, 8, @weekStart), 'HVAC', 'Cancelled', NULL, 0, GETDATE()),
|
||||
('SPIKE-STATUS-006', 'Status spike - UnAssigned legado','SS6', 'UnAssigned', 'Medium', DATEADD(day, 5, @weekStart), DATEADD(day, 7, @weekStart), 'Plumbing', 'Unassigned', NULL, 0, GETDATE()),
|
||||
-- Canônico API (enum ToString)
|
||||
('SPIKE-STATUS-007', 'Status spike - Open canônico', 'SS7', 'Open', 'Medium', DATEADD(day, 0, @weekStart), DATEADD(day, 5, @weekStart), 'HVAC', 'Repair', NULL, 0, GETDATE()),
|
||||
('SPIKE-STATUS-008', 'Status spike - InProgress canônico','SS8', 'InProgress', 'High', DATEADD(day, 1, @weekStart), DATEADD(day, 6, @weekStart), 'Plumbing', 'Repair', NULL, 0, GETDATE()),
|
||||
('SPIKE-STATUS-009', 'Status spike - OnHold canônico', 'SS9', 'OnHold', 'Low', DATEADD(day, 2, @weekStart), DATEADD(day, 10, @weekStart), 'Electrical', 'Hold', NULL, 0, GETDATE()),
|
||||
('SPIKE-STATUS-010', 'Status spike - Completed canônico','SS10','Completed', 'Low', DATEADD(day, 3, @weekStart), DATEADD(day, 4, @weekStart), 'General', 'Complete', NULL, 0, GETDATE()),
|
||||
('SPIKE-STATUS-011', 'Status spike - Cancelled canônico','SS11','Cancelled', 'Low', DATEADD(day, 4, @weekStart), DATEADD(day, 8, @weekStart), 'HVAC', 'Cancelled', NULL, 0, GETDATE()),
|
||||
-- Open sem assignee (derivado Unassigned no board)
|
||||
('SPIKE-STATUS-012', 'Status spike - Open unassigned', 'SS12','Open', 'Medium', DATEADD(day, 1, @weekStart), DATEADD(day, 3, @weekStart), 'HVAC', 'Repair', NULL, 0, GETDATE()),
|
||||
-- Past due candidato (scheduled no passado, status não terminal)
|
||||
('SPIKE-STATUS-013', 'Status spike - past due candidate','SS13','In Progress', 'High', DATEADD(day, -3, @weekStart), DATEADD(day, -1, @weekStart), 'HVAC', 'Overdue test', NULL, 0, GETDATE());
|
||||
|
||||
PRINT 'Seed status spike: 13 work orders inseridos (prefixo SPIKE-STATUS-).';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Seed status spike: já existente — pulando INSERT.';
|
||||
GO
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Distribuição Status (todos os WOs não-template)
|
||||
-- ---------------------------------------------------------------------------
|
||||
PRINT '--- Distribuição Status ---';
|
||||
SELECT Status, COUNT(*) AS Cnt
|
||||
FROM workOrders
|
||||
WHERE istemplate IS NULL OR istemplate = 0
|
||||
GROUP BY Status
|
||||
ORDER BY Cnt DESC;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Status x assignee (detectar Unassigned implícito)
|
||||
-- ---------------------------------------------------------------------------
|
||||
PRINT '--- Status x AssignTo (Unassigned implícito) ---';
|
||||
SELECT
|
||||
Status,
|
||||
CASE WHEN AssignTo IS NULL OR LTRIM(RTRIM(AssignTo)) = '' THEN 1 ELSE 0 END AS IsUnassigned,
|
||||
COUNT(*) AS Cnt
|
||||
FROM workOrders
|
||||
WHERE istemplate IS NULL OR istemplate = 0
|
||||
GROUP BY Status,
|
||||
CASE WHEN AssignTo IS NULL OR LTRIM(RTRIM(AssignTo)) = '' THEN 1 ELSE 0 END
|
||||
ORDER BY Status, IsUnassigned;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Órfãos — valores não mapeáveis pelo mapper G2 (ajustar lista conforme spike)
|
||||
-- ---------------------------------------------------------------------------
|
||||
PRINT '--- Status órfãos (fora do vocabulário conhecido) ---';
|
||||
SELECT Status, COUNT(*) AS Cnt
|
||||
FROM workOrders
|
||||
WHERE (istemplate IS NULL OR istemplate = 0)
|
||||
AND Status IS NOT NULL
|
||||
AND LTRIM(RTRIM(Status)) NOT IN (
|
||||
'Open', 'In Progress', 'On Hold', 'Done', 'Cancelled', 'UnAssigned', 'Unassigned',
|
||||
'InProgress', 'OnHold', 'Completed',
|
||||
'new', 'assigned', 'in_progress', 'on_hold', 'completed', 'cancelled', 'unknown'
|
||||
)
|
||||
GROUP BY Status
|
||||
ORDER BY Cnt DESC;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Template query PRODUÇÃO (rodar manualmente em prod/staging)
|
||||
-- ---------------------------------------------------------------------------
|
||||
PRINT '--- Template query PRODUÇÃO ---';
|
||||
-- SELECT Status, COUNT(*) AS Cnt FROM workOrders WHERE istemplate IS NULL OR istemplate = 0 GROUP BY Status ORDER BY Cnt DESC;
|
||||
|
||||
GO
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
-- Spike WorkOrderType: coluna legada + seed local + queries de análise
|
||||
-- Executar APÓS dotnet ef database update
|
||||
-- Uso: sqlcmd -S localhost,1433 -U sa -P "SeaHaven_Dev_2026!" -d SeahavenIndustries -C -i scripts/spike-work-order-type.sql
|
||||
|
||||
SET NOCOUNT ON;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Colunas legadas (existem em prod/db.txt, ausentes nas migrations EF)
|
||||
-- ---------------------------------------------------------------------------
|
||||
IF COL_LENGTH('workOrders', 'WorkOrderType') IS NULL
|
||||
ALTER TABLE workOrders ADD WorkOrderType varchar(50) NULL;
|
||||
|
||||
IF COL_LENGTH('workOrders', 'AvettaTask') IS NULL
|
||||
ALTER TABLE workOrders ADD AvettaTask varchar(max) NULL;
|
||||
|
||||
IF COL_LENGTH('workOrders', 'AssignDate') IS NULL
|
||||
ALTER TABLE workOrders ADD AssignDate date NULL;
|
||||
|
||||
GO
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Seed spike (somente se ainda não existir prefixo SPIKE-WO-)
|
||||
-- ---------------------------------------------------------------------------
|
||||
IF NOT EXISTS (SELECT 1 FROM workOrders WHERE InternalWONumber LIKE 'SPIKE-WO-%')
|
||||
BEGIN
|
||||
DECLARE @weekStart date = CAST(GETDATE() AS date);
|
||||
DECLARE @weekEnd date = DATEADD(day, 6, @weekStart);
|
||||
|
||||
INSERT INTO workOrders (
|
||||
InternalWONumber, WorkerOrderTitle, SiteCode, Status, Priority,
|
||||
ScheduledDate, DueDate, Trade, Problem, WorkOrderType, istemplate, CreatedDate
|
||||
)
|
||||
VALUES
|
||||
-- NULL WorkOrderType (5)
|
||||
('SPIKE-WO-001', 'Spike seed - null type A', 'BK5', 'Open', 'Medium', DATEADD(day, 0, @weekStart), DATEADD(day, 3, @weekStart), 'HVAC', 'Cooling', NULL, 0, GETDATE()),
|
||||
('SPIKE-WO-002', 'Spike seed - null type B', 'BK6', 'Open', 'Low', DATEADD(day, 1, @weekStart), DATEADD(day, 5, @weekStart), 'Plumbing', 'Leak', NULL, 0, GETDATE()),
|
||||
('SPIKE-WO-003', 'Spike seed - null type C', 'BK7', 'InProgress', 'Medium', DATEADD(day, 2, @weekStart), DATEADD(day, 4, @weekStart), 'Electrical', 'Panel', NULL, 0, GETDATE()),
|
||||
('SPIKE-WO-004', 'Spike seed - null type D', 'BK8', 'Open', 'High', DATEADD(day, 3, @weekStart), DATEADD(day, 7, @weekStart), 'HVAC', 'Filter', NULL, 0, GETDATE()),
|
||||
('SPIKE-WO-005', 'Spike seed - null type E', 'BK9', 'OnHold', 'Low', DATEADD(day, 4, @weekStart), DATEADD(day, 10, @weekStart), 'General', 'Inspection', NULL, 0, GETDATE()),
|
||||
-- PM (5)
|
||||
('SPIKE-WO-006', 'Spike seed - PM A', 'PM1', 'Open', 'Medium', DATEADD(day, 0, @weekStart), DATEADD(day, 14, @weekStart), 'HVAC', 'Preventive Maintenance', 'PM', 0, GETDATE()),
|
||||
('SPIKE-WO-007', 'Spike seed - PM B', 'PM2', 'Open', 'Medium', DATEADD(day, 1, @weekStart), DATEADD(day, 14, @weekStart), 'Plumbing', 'PM Schedule', 'PM', 0, GETDATE()),
|
||||
('SPIKE-WO-008', 'Spike seed - PM C', 'PM3', 'InProgress', 'Low', DATEADD(day, 2, @weekStart), DATEADD(day, 21, @weekStart), 'Electrical', 'Quarterly PM', 'PM', 0, GETDATE()),
|
||||
('SPIKE-WO-009', 'Spike seed - PM D', 'PM4', 'Open', 'Medium', DATEADD(day, 5, @weekStart), DATEADD(day, 30, @weekStart), 'HVAC', 'Annual PM', 'PM', 0, GETDATE()),
|
||||
('SPIKE-WO-010', 'Spike seed - PM E', 'PM5', 'Open', 'Low', DATEADD(day, 6, @weekStart), DATEADD(day, 7, @weekStart), 'General', 'PM', 'PM', 0, GETDATE()),
|
||||
-- Standard (4)
|
||||
('SPIKE-WO-011', 'Spike seed - Standard A', 'ST1', 'Open', 'Medium', DATEADD(day, 0, @weekStart), DATEADD(day, 5, @weekStart), 'HVAC', 'Repair', 'Standard', 0, GETDATE()),
|
||||
('SPIKE-WO-012', 'Spike seed - Standard B', 'ST2', 'InProgress', 'High', DATEADD(day, 2, @weekStart), DATEADD(day, 6, @weekStart), 'Plumbing', 'Repair', 'Standard', 0, GETDATE()),
|
||||
('SPIKE-WO-013', 'Spike seed - Standard C', 'ST3', 'Open', 'Medium', DATEADD(day, 4, @weekStart), DATEADD(day, 8, @weekStart), 'Electrical', 'Repair', 'Standard', 0, GETDATE()),
|
||||
('SPIKE-WO-014', 'Spike seed - Standard D', 'ST4', 'Completed', 'Low', DATEADD(day, 1, @weekStart), DATEADD(day, 2, @weekStart), 'General', 'Repair', 'Standard', 0, GETDATE()),
|
||||
-- Add-On (3)
|
||||
('SPIKE-WO-015', 'Spike seed - Add-On A', 'AO1', 'Open', 'Medium', DATEADD(day, 3, @weekStart), DATEADD(day, 9, @weekStart), 'HVAC', 'Add work', 'Add-On', 0, GETDATE()),
|
||||
('SPIKE-WO-016', 'Spike seed - Add-On B', 'AO2', 'Open', 'High', DATEADD(day, 4, @weekStart), DATEADD(day, 10, @weekStart), 'Plumbing', 'Add work', 'Add-On', 0, GETDATE()),
|
||||
('SPIKE-WO-017', 'Spike seed - Add-On C', 'AO3', 'InProgress', 'Medium', DATEADD(day, 5, @weekStart), DATEADD(day, 11, @weekStart), 'Electrical', 'Add work', 'Add-On', 0, GETDATE()),
|
||||
-- Emergency + Corrective (3)
|
||||
('SPIKE-WO-018', 'Spike seed - Emergency', 'EM1', 'Open', 'Critical', DATEADD(day, 0, @weekStart), DATEADD(day, 1, @weekStart), 'HVAC', 'Emergency', 'Emergency', 0, GETDATE()),
|
||||
('SPIKE-WO-019', 'Spike seed - Corrective A', 'CR1', 'Open', 'High', DATEADD(day, 2, @weekStart), DATEADD(day, 4, @weekStart), 'Plumbing', 'Corrective', 'Corrective', 0, GETDATE()),
|
||||
('SPIKE-WO-020', 'Spike seed - Corrective B', 'CR2', 'Open', 'Medium', DATEADD(day, 6, @weekStart), DATEADD(day, 8, @weekStart), 'General', 'Corrective', 'Corrective', 0, GETDATE());
|
||||
|
||||
PRINT 'Seed spike: 20 work orders inseridos (prefixo SPIKE-WO-).';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Seed spike: já existente — pulando INSERT.';
|
||||
GO
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Queries de análise (entregável spike)
|
||||
-- ---------------------------------------------------------------------------
|
||||
PRINT '--- Distribuição WorkOrderType (seed + existentes) ---';
|
||||
SELECT WorkOrderType, COUNT(*) AS Cnt
|
||||
FROM workOrders
|
||||
WHERE istemplate IS NULL OR istemplate = 0
|
||||
GROUP BY WorkOrderType
|
||||
ORDER BY Cnt DESC;
|
||||
|
||||
PRINT '--- % populado ---';
|
||||
SELECT
|
||||
COUNT(*) AS TotalRows,
|
||||
SUM(CASE WHEN WorkOrderType IS NOT NULL AND LTRIM(RTRIM(WorkOrderType)) <> '' THEN 1 ELSE 0 END) AS Populated,
|
||||
CAST(100.0 * SUM(CASE WHEN WorkOrderType IS NOT NULL AND LTRIM(RTRIM(WorkOrderType)) <> '' THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0) AS decimal(5,2)) AS PopulatedPct
|
||||
FROM workOrders
|
||||
WHERE istemplate IS NULL OR istemplate = 0;
|
||||
|
||||
PRINT '--- Cruzamento Problem/Trade (hipótese derivação) ---';
|
||||
SELECT WorkOrderType, Problem, Trade, COUNT(*) AS Cnt
|
||||
FROM workOrders
|
||||
WHERE (istemplate IS NULL OR istemplate = 0)
|
||||
AND (InternalWONumber LIKE 'SPIKE-WO-%' OR WorkOrderType IS NOT NULL)
|
||||
GROUP BY WorkOrderType, Problem, Trade
|
||||
ORDER BY WorkOrderType, Cnt DESC;
|
||||
|
||||
PRINT '--- Template query PRODUÇÃO (rodar manualmente em prod/staging) ---';
|
||||
-- SELECT WorkOrderType, COUNT(*) FROM workOrders GROUP BY WorkOrderType;
|
||||
-- SELECT COUNT(*) AS Total, SUM(CASE WHEN WorkOrderType IS NOT NULL THEN 1 ELSE 0 END) AS WithType FROM workOrders;
|
||||
|
||||
GO
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
# Volume Discovery — Work Orders
|
||||
# Executa queries do volume-discovery-report.md contra staging/prod read-only.
|
||||
#
|
||||
# Uso:
|
||||
# $env:SQL_CONNECTION_STRING = "Server=...;Database=...;User Id=...;Password=...;TrustServerCertificate=True"
|
||||
# .\scripts\volume-discovery.ps1
|
||||
|
||||
param(
|
||||
[string]$ConnectionString = $env:SQL_CONNECTION_STRING
|
||||
)
|
||||
|
||||
function Get-SqlConnectionArgs([string]$cs) {
|
||||
return @("-C", $cs)
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ConnectionString)) {
|
||||
Write-Error "Defina SQL_CONNECTION_STRING ou passe -ConnectionString."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$queries = @{
|
||||
TotalWorkOrders = @"
|
||||
SELECT COUNT(*) AS TotalWorkOrders
|
||||
FROM workOrders
|
||||
WHERE IsDeleted IS NULL OR IsDeleted = 0;
|
||||
"@
|
||||
P95ProxyMaxPerWeek = @"
|
||||
WITH WeeklyCounts AS (
|
||||
SELECT DATEPART(iso_week, ScheduledDate) AS IsoWeek,
|
||||
YEAR(ScheduledDate) AS IsoYear,
|
||||
COUNT(*) AS Cnt
|
||||
FROM workOrders
|
||||
WHERE ScheduledDate IS NOT NULL
|
||||
AND (IsDeleted IS NULL OR IsDeleted = 0)
|
||||
GROUP BY DATEPART(iso_week, ScheduledDate), YEAR(ScheduledDate)
|
||||
)
|
||||
SELECT MAX(Cnt) AS P95ProxyMaxPerWeek,
|
||||
AVG(Cnt * 1.0) AS AvgPerWeek
|
||||
FROM WeeklyCounts;
|
||||
"@
|
||||
UnscheduledCount = @"
|
||||
SELECT COUNT(*) AS UnscheduledCount
|
||||
FROM workOrders
|
||||
WHERE ScheduledDate IS NULL
|
||||
AND (IsDeleted IS NULL OR IsDeleted = 0)
|
||||
AND Status NOT IN ('Completed', 'Complete', 'Cancelled', 'Canceled');
|
||||
"@
|
||||
}
|
||||
|
||||
$connArgs = Get-SqlConnectionArgs $ConnectionString
|
||||
|
||||
Write-Host "=== Volume Discovery ===" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
foreach ($name in $queries.Keys) {
|
||||
Write-Host "--- $name ---" -ForegroundColor Yellow
|
||||
sqlcmd @connArgs -Q $queries[$name] -W
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
Write-Host "Preencha docs/work-orders/phase-0/volume-discovery-report.md e docs/work-orders/phase-4/search-tier-decision.md com os resultados."
|
||||
Loading…
Add table
Reference in a new issue