mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-01 14:33:15 +00:00
182 lines
6.8 KiB
Markdown
182 lines
6.8 KiB
Markdown
|
|
# 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`.
|