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