# Melhoria: Listagem de Associados da Regional via JSON no Front-end

**Data:** 2026-09-03
**Ambiente:** local (`portal_sucesu_app`)
**Módulo:** `V2/Modules/Conexoes/UnidadeRegional`
**Status:** implementado e verificado (200 associados de demonstração vinculados à regional `sucesu-distrito-federal`)

## Contexto / Problema

A listagem de associados na página pública de cada regional (`/regional/{slug}`,
`UnidadeRegionalPortalController::show`) renderizava **todos os cards no servidor**
(`@foreach` sobre a coleção `$associados`). Isso trazia três limitações:

1. **Sem paginação** — todos os itens iam ao DOM de uma vez, sem teto.
2. **Sem busca** — não havia filtro rápido de associados no portal.
3. **DOM pesado** — mesmo itens fora da viewport eram montados, e a busca teria de
   mostrar/ocultar nós manualmente.

## Solução

O servidor passa a enviar **todos os metadados dos associados num único payload JSON**
embutido na página. A paginação ("Ver mais") e a busca passam a ser **100% client-side**,
e as imagens são carregadas **sob demanda** (render incremental + `loading="lazy"`).

### Comportamento
- Exibe os **20 mais recentes** (ordenados por `created_at` desc no controller).
- Botão **"Ver mais"** carrega o próximo lote de 20 (append incremental, sem reload).
- **Busca por regex** sobre `nome + sigla + cidade/segmento`, em tempo real no `input`.
- **Imagens sob demanda:** só o lote visível entra no DOM; `<img loading="lazy">`.

## Arquivos Alterados

| Arquivo | Mudança |
|---|---|
| `V2/Modules/Conexoes/UnidadeRegional/Controllers/UnidadeRegionalPortalController.php` | `$associados` (ativo + `sortByDesc('created_at')`) mapeado para DTO `['id','name','logo','search']`; gerado `$associadosJson` via `json_encode(... JSON_HEX_TAG\|JSON_HEX_AMP\|JSON_HEX_APOS\|JSON_HEX_QUOT)` com `str_replace('</','<\/', ...)`; passado à view junto de `$associados` (para `@if count>20`). |
| `V2/Modules/Conexoes/UnidadeRegional/Views/portal/show.blade.php` | Seção Associados: mantido o campo de busca (estética `/associados` — `.search-form-group`/`.search-form-input`/`.search-form-btn`); o `@foreach` foi substituído por `<div id="ur-associados-grid">` + `<script type="application/json" id="ur-associados-data">`. Botão "Ver mais" (`#ur-associados-load-more`) renderizado quando `count > 20`. Modal de detalhe XHR (`data-id` → `v2.associados.portal.detalhe`) **inalterado**. CSS de busca já existia em `@push('styles')`. |
| `tests/Feature/V2/Modules/Conexoes/UnidadeRegional/UnidadeRegionalPortalTest.php` | Testes deixaram de contar nós DOM (causa de flakiness); passaram a validar o blob JSON (`assertSee` dos nomes, `substr_count('"id":') >= 25`), o campo de busca e o botão "Ver mais"; o teste de inativos garante 1 ativo no JSON e ausência dos inativos. |

## Detalhe Técnico

**Controller — DTO `search` (busca composta, lowercase no servidor):**
```php
$search = implode(' ', array_filter([
    $e->name,
    $e->category,
    optional($e->metadata)['city'] ?? optional($e->metadata)['cidade'] ?? '',
    $unidade->acronym, $unidade->state, $unidade->name,
    $e->tags->pluck('name')->implode(' '),
]));
// 'search' => mb_strtolower($search)
```

**View — payload e container:**
```blade
<div id="ur-associados-grid" class="row g-3 g-lg-4 unidade-regional-associados-grid"></div>
<script type="application/json" id="ur-associados-data">{!! $associadosJson !!}</script>
@if($associados->count() > 20)
    <button type="button" id="ur-associados-load-more" class="btn btn-outline-primary rounded-pill px-4">
        Ver mais <i class="bi bi-chevron-down ms-1"></i>
    </button>
@endif
```

**JS (`@push('scripts')`):**
- `DATA = JSON.parse(#ur-associados-data)`; `PAGE = 20`; `visible = PAGE`; `filtered = DATA`.
- `cardHtml(item)` monta a coluna com `unidade-regional-associado-item`, o card
  `.unidade-regional-associado-card[role=button][data-id][data-bs-toggle=modal]`, logo
  `<img src loading="lazy">` (ou ícone) e o nome — preservando o `data-id` para o modal existente.
- `render()` insere `filtered.slice(0, visible)`, atualiza o badge `#ur-associados-count` e mostra/esconde
  `#ur-associados-load-more` (`classList.toggle('d-none')`).
- "Ver mais" → `visible += PAGE; render()`; busca → `new RegExp(val,'i')` (fallback literal) filtrando
  `i.search`; `visible = PAGE; render()`.

> O JSON trafega todos os metadados da regional (dezenas/centenas de associados por regional) —
> adequado ao caso de uso. O modal de detalhe continua via XHR para manter o payload enxuto.

## Verificação

- `phpunit --filter UnidadeRegionalPortalTest` → **OK (12 tests, 37 assertions)**.
- `GET /regional/sucesu-distrito-federal` → **200**, com `ur-associados-data`, `search-form-group`
  e `ur-associados-load-more` no HTML; o blob contém 203 associados (`"id":` ×203) após vincular
  200 de demonstração (com imagens locais em `/storage/demo/assoc-demo-*.png`).

## Lição / Prevenção

- Para listas no portal que podem crescer, prefira **enviar metadados como JSON e renderizar no
  cliente** em vez de `@foreach` server-side: habilita paginação/busca O(1) front-end sem novas
  queries e mantém o DOM leve (imagens `loading="lazy"`).
- Ao embutir JSON em `<script>`, sempre usar `JSON_HEX_*` e sanitizar `</` para evitar quebra de
  script por conteúdo malicioso/acentuado.
- Testes de feature que dependem de contagem de nós no HTML são frágeis (cache de view/OPcache);
  prefira afirmar o **conteúdo do blob JSON**.
