# FEATURES.md — Funcionalidades do domínio (cadastro de empresa, galeria, paginação)

**Escopo:** documenta funcionalidades de domínio que entram **neste ato de divisão** do monólito
Rota Viva (ver `EPIC.md`, `PLAN_BACKEND.md`, `PLAN_FRONTEND.md`). Cada item aponta para o plano de
backend (`/api/v1`) e para o front-end single-tenant (**Laravel 5.5 + Blade + Bootstrap 4**, CSS
próprio só quando necessário). As telas de gestor e visitante consomem a API; o front Blade é um
thin-client que só renderiza e faz HTTP.

> Restrições de stack (ver `GUIDES/laravel_guide/55/SKILL.md`): Laravel 5.5.50 / PHP 7.2. Usar
> `{{ csrf_field() }}` (não `@csrf`), `config/app.php` (não `config/logging.php`), `Hash::make`
> (bcrypt), `Route::resource` (não `apiResource`), `Resource::collection` para paginação, e evitar
> arrow functions / typed properties / union types. PostgreSQL 10.23: preferir `jsonb`, não usar
> `gen_random_uuid()` no banco, evitar modificadores `->after()`.

---

## F-A · Cadastro de empresa (auto-cadastro do empreendedor)

**Objetivo:** abrir, na área do visitante, uma tela onde a **própria empresa** se cadastra como
ponto turístico, informando localização, dados cadastrais, faixa de preço, elementos de
acessibilidade, categoria e foto principal — tornando-se um atrativo consultável no catálogo.

**Relaciona-se com:** `EPIC.md` F-5 (migrar cadastro de empreendedor) e `B-6`/`B-7` (controllers/
form requests de API). Estende o `POST /api/v1/empreendedores` já previsto.

### Contrato de API
- `POST /api/v1/empreendedores` — cria o registro (tenant escopado via `X-Tenant`).
  - Payload (allowlist no Form Request):
    - `nome` (string, required, max:255)
    - `categoria_id` (int, required — referencia `catalogos`/`atrativos`; ex.: hospedagem,
      alimentação, cultura, natureza, aventura)
    - `descricao` (string, required, min:10, max:2000)
    - `endereco` (string, required), `cidade`/`uf` (string)
    - `latitude` / `longitude` (decimal, required — coordenadas; sem PostGIS no PG 10.23)
    - `faixa_preco` (enum: `$`, `$$`, `$$$`; mapeia para `1`/`2`/`3` no banco)
    - `acessibilidade` (array de chaves: `cadeirante`, `braile`, `piso_tatil`, `audiodescricao`,
      `banheiro_adaptado`, `estacionamento_adaptado`, …) — armazenado como `jsonb`
    - `contato_email` / `contato_telefone` / `site` (validados; `email`/`url`/`telefone`)
    - `foto_principal` (upload autenticado via endpoint assinado/proxy — ver `B-11`; o front envia
      o arquivo e recebe a URL, que é gravada aqui)
- `GET /api/v1/empreendedores/{id}` — leitura (inclui URL da foto e lista de acessibilidade).
- Resposta serializada por `EntrepreneurResource` (`B-5`).

### Front-end (Blade + Bootstrap 4)
- Tela `entrepreneurs` (`resources/views/entrepreneurs.blade.php`): form com `{{ csrf_field() }}`,
  `old()` em todos os campos, `<label for>` explícita, `fieldset/legend` no grupo de acessibilidade,
  `required`+`*`, `aria-invalid`+`aria-describedby`, erro como `role="alert"` e resumo no topo.
- Faixa de preço: `select` com opção vazia (`datalist`/`select`).
- Acessibilidade: grupo de `checkbox` Bootstrap (`form-check`) dentro de `fieldset`.
- Categoria: `select` poblado via `GET /api/v1/catalogos` (seções) ou tabela de categorias.
- Upload de foto: `<input type="file" accept="image/*">` enviado ao endpoint assinado; preview
  via `URL.createObjectURL` (JS mínimo, `defer`); **nunca** `localStorage` para o token.
- Tratar `429` do throttle com `role="alert"` (ver `PLAN_FRONTEND.md` §4).

### Backend (Laravel 5.5)
- Form Request `StoreEmpreendedor` com allowlist + `messages()`/`attributes()` pt-BR.
- `Empreendedor` (ou `Atrativo`) com `$fillable` + `$casts['acessibilidade'=>'array']`
  (jsonb no PG 10.23), `latitude`/`longitude` decimal.
- Global scope `where municipio_id` (anti cross-tenant — `B-13`).
- Upload: endpoint assinado/proxy grava em `Storage::disk('public')` (fora do webroot), allowlist de
  extensão (`image/*`), retorna URL.

**Aceite:** empresa consegue se auto-cadastrar como ponto turístico com local, preço, acessibilidade,
categoria e foto; registro aparece no catálogo do tenant; dados validados na borda.

---

## F-B · Galeria de fotos do ponto turístico

**Objetivo:** além da foto principal (F-A), cada empresa/ponto turístico possui uma **galeria** de
fotos, visível na aba do perfil empresa-turista (tela `detail` do visitante).

**Relaciona-se com:** `F-A` (foto principal) e `B-6`/`B-11` (mídia via API).

### Contrato de API
- `POST /api/v1/empreendedores/{id}/fotos` — adiciona foto à galeria (upload assinado → URL).
- `GET /api/v1/empreendedores/{id}` — passa `fotos` como coleção (`EntrepreneurResource` inclui
  `fotos: FotoResource::collection($this->whenLoaded('fotos'))`).
- `DELETE /api/v1/empreendedores/{id}/fotos/{foto_id}` — remoção (gestor/proprietário).
- Modelo `Foto` (ou `Midia`): `empreendedor_id`, `url`, `ordem`, `legenda?`, `capa?` (bool).

### Front-end (Blade + Bootstrap 4)
- Na aba "empresa-turista" (`detail`): carrossel/galeria com componentes Bootstrap 4
  (`carousel` ou grid de `card`/`img-thumbnail`), `object-fit: cover` + `aspect-ratio` nas imagens,
  `alt` descritivo (acessibilidade). Lazy `loading="lazy"` + `decoding="async"` abaixo da dobra.
- Form de upload da galeria reusa o componente de upload da F-A; preview em grade.

### Backend (Laravel 5.5)
- Relação `Empreendedor hasMany Foto`; `FotoResource` condicional.
- Upload assinado/proxy (`B-11`); capa definida por flag `capa` (uma por empresa).
- Paginação/limite de fotos por tenant (anti-abuso de storage).

**Aceite:** galeria visível na aba da empresa-turista; upload/remoção funcionam via API assinada;
foto principal e galeria distintas.

---

## F-C · Paginação nas listagens da área do gestor

**Objetivo:** todos os pontos da área do gestor que apresentam **listagens** — "Maior procura",
"Oportunidades", "Recursos mais solicitados", e CRUD de módulos — devem usar **paginação** explícita,
evitando rolagem infinita (scroll infinito) para manter performance, acessibilidade e previsibilidade.

**Relaciona-se com:** `EPIC.md` F-8 (dashboard de análises), F-9 (gestão/CRUD) e `B-5`/`B-6`.

### Contrato de API
- `GET /api/v1/gestor/dashboard` — as sub-listas analíticas (`maior_procura`, `oportunidades`,
  `recursos_mais_solicitados`) retornam como **paginadas**: `data[]` + `meta`
  (`current_page`, `last_page`, `per_page`, `total`, `from`, `to`). Parâmetros `?page=` e `?per_page=`.
- `GET /api/v1/gestor/{module}` — listas de módulos paginadas (mesmo envelope).
- Backend usa `paginate()` do Eloquent; serialização via `Resource::collection($paginator)` (5.5
  inclui metadados de paginação). Default `per_page` por tenant (ex.: 15).

### Front-end (Blade + Bootstrap 4)
- Renderizar o componente de **paginação do Bootstrap 4** (`.pagination .justify-content-center`)
  a partir de `meta`, com links `?page=` nomeados (`route()`), `aria-label` na navegação e
  `aria-current="page"` na página ativa. Nunca scroll infinito nessas telas.
- Estado de carregamento via skeleton (CSS mínimo) + `aria-live="polite"`.
- Respeitar `prefers-reduced-motion` nas transições de página.

### Backend (Laravel 5.5)
- `DashboardResource`/`ModuleResource` com coleções paginadas; `per_page` validado (allowlist:
  máx. 50) no Form Request de filtro.
- `where municipio_id` obrigatório (isolamento).

**Aceite:** listagens do gestor paginadas (sem rolagem infinita); controles de página acessíveis;
`meta` consistente entre dashboard e CRUD.

---

## F-D · Adaptações contextuais da rota (mais opções além de "chuva")

**Objetivo:** a tela de adaptação de roteiro (`route-adaptation`) hoje só oferece "começou a chover".
Ampliar para **múltiplas opções contextuais** que reconfiguram o roteiro conforme a necessidade do
cidadão — ex.: **Banheiro**, **Livre acesso para cadeira de rodas**, **Ar livre**, e outras
(extensível). Além disso, a **página da rota adaptada** (`route-result` da adaptação) deve expor os
links do **Google Maps e Waze** em texto para cada parada, na mesma forma já usada na rota principal.

**Relaciona-se com:** `EPIC.md` F-4 (criação/adaptação de roteiro) e `B-6`/`RouteAdaptationService`.

### Contrato de API
- Generalizar o endpoint de adaptação (hoje `POST /api/v1/roteiros/{id}/adaptar/chuva`) para um
  único, parametrizável:
  - `POST /api/v1/roteiros/{id}/adaptar` com body `contextos` (array de chaves). Ex.:
    - `chuva` — prioriza paradas cobertas / indoor (já existente).
    - `banheiro` — inclui/prioriza pontos com banheiro.
    - `cadeirante` — filtra por `acessibilidade` contendo `cadeirante`/`banheiro_adaptado`/
      `estacionamento_adaptado` (ver F-A).
    - `ar_livre` — prioriza atrativos ao ar livre.
    - `sombra`, `custo_baixo`, `kid_friendly`, … (extensível via tabela/enum de contextos).
  - Resposta via `AdaptacaoResource`, que estende `RoteiroResource` e **sempre** inclui, por parada:
    `maps_url` (link Google Maps `https://www.google.com/maps/search/?api=1&query=<lat>,<lng>` ou
    `dir` com origem) e `waze_url` (`https://waze.com/ul?ll=<lat>,<lng>&navigate=yes`), além de
    `endereco` legível.
- Backend aplica os filtros no `RouteAdaptationService` (reescrito para aceitar N contextos, não só
  chuva) respeitando `where municipio_id` (isolamento).

### Front-end (Blade + Bootstrap 4)
- `route-adaptation`: grupo de `checkbox` Bootstrap (`form-check`) "O que mudou?" / "Adapte para:"
  (Banheiro, Cadeirante, Ar livre, Chuva, …), dentro de `fieldset/legend`, `aria-describedby`.
  Múltipla seleção permitida; envio via JS mínimo (`defer`) com `X-Tenant` + `Authorization`.
- `route-result` (adaptado): para cada parada, renderizar em texto os links
  **Google Maps** e **Waze** (âncoras `<a target="_blank" rel="noopener noreferrer">`), com
  `aria-label` descritivo e ícone inline SVG `aria-hidden`. **Paridade obrigatória** com a rota
  principal (mesma marcação/macro Blade, ex.: `@include('components.map-links')`).

### Backend (Laravel 5.5)
- `RouteAdaptationService::adaptar(Roteiro $roteiro, array $contextos)` — switch por contexto.
- `AdaptacaoResource` reutiliza helper de links de mapa (centralizado, usado também na rota
  principal) para garantir paridade.
- Validação na borda (Form Request): `contextos` array, cada item no allowlist de contextos.

**Aceite:** cidadão adapta roteiro por N contextos (não só chuva); rota adaptada expõe Google Maps e
Waze em texto por parada, idêntico à rota principal.

---

## F-E · Offline-first (cache do device: pontos turísticos + rotas criadas)

**Objetivo (crítico):** **offline-first em tudo que for possível**. O cidadão **não** pode criar
rota offline, mas deve poder, **sem login** e **vinculado ao device**, (1) ver as rotas que ele
criou e (2) ver todos os pontos turísticos (endereço, nome, faixa de preço, acessibilidade,
categoria, coordenadas, links de mapa) — **sem as fotos**. Tudo deve ser carregado em *background*
durante a navegação, para que o device já tenha as informações e funcione mesmo sem internet.

**Relaciona-se com:** `EPIC.md` F-3 (catálogo/mapa), F-4 (roteiros) e `F-13` (offline-first).

### Contrato de API
- **Device ID (anon, sem login):** o cliente gera um UUID uma vez e persiste em `localStorage`
  (`rv_device_id`); envia como header `X-Device-Id` na criação e na leitura "minhas rotas".
- **Criação (só online):** `POST /api/v1/roteiros` exige rede + `X-Device-Id` válido; o backend
  grava `device_id` (anon). **Offline → criar é bloqueado** (ver front).
- **Ler minhas rotas (offline OK):** `GET /api/v1/roteiros?device_id={id}` (ou via header) —
  escopado por `where device_id`, serializado por `RoteiroResource`. Não expõe rotas de outro
  device.
- **Catálogo/pontos (offline OK):** `GET /api/v1/catalogos/{section}[/{slug}]` e `GET /api/v1/mapa`
  — GETs cacheáveis.
- **Sem fotos no payload offline:** os Resources retornam **só metadados** (URLs de imagem, nunca
  binário/base64). O cache nunca armazena respostas de imagem — só JSON de metadados.

### Front-end (Blade + Bootstrap 4, JS mínimo)
- **Service Worker** `public/sw.js` registrado via partial (`@include('components.sw-register')`),
  estratégia:
  - Assets estáticos (CSS/JS/Bootstrap): *cache-first* (precache no install).
  - API GET `GET /api/v1/...`: *network-first* com fallback de cache (stale-while-revalidate) —
    offline serve o último cache; *runtime cache*.
  - `POST` (criar rota): **sempre network** — sem criação offline.
- **Warm-up em background:** no carregamento (`defer`), prefetch do catálogo + pontos + "minhas
  rotas" (se houver device id) para popular o cache do SW; revalida em evento `online`.
- **Device ID:** gerado uma vez em JS e persistido em `localStorage`; a lista "suas rotas" é lida
  do cache quando offline.
- **UI offline:** se offline e o cidadão tenta criar rota, desabilitar o submit + `role="alert"`
  ("Você precisa de conexão para criar a rota"); pontos e rotas cached são exibidos normalmente
  (componentes Bootstrap 4, `.sr-only`/aria para estado).
- **Não cachear imagens:** o SW ignora respostas `content-type: image/*`; só JSON de metadados.

### Backend (Laravel 5.5)
- Migration `roteiros.device_id` (string, índice) — PG 10.23: sem `->after()`, usar `jsonb`/
  `string` padrão.
- Global scope `where municipio_id` (isolamento) + scope de device para leituras anon.
- Form Request `StoreRoteiro`: `device_id` required (vem de `X-Device-Id`), allowlist, formato
  UUID (`uuid` rule). Leitura "minhas rotas" só retorna linhas do `device_id` informado (sem
  vazamento cross-device); `throttle` restrito.
- `RoteiroResource`/`CatalogResource`: metadados apenas (URLs), sem binário de imagem.

**Aceite:** cidadão abre o app online uma vez → pontos + suas rotas em cache; fica offline →
navega pontos (endereço/nome/preço/acessibilidade/coords/links) e suas rotas criadas; **não** cria
rota offline (bloqueado + aviso); prefetch em background mantém o cache fresco na reconexão.

---

## Notas de integração com a divisão

- F-A e F-B estendem o fluxo `empreendedores` (F-5) e o endpoint de mídia (B-11).
- F-C aplica-se a todas as leituras `gestor/*` (F-8, F-9) — paginação é requisito transversal.
- F-D estende a adaptação de roteiro (F-4): múltiplos contextos além de "chuva" e paridade de links
  Google Maps/Waze na rota adaptada.
- F-E (offline-first, crítico) aplica-se a F-3 (catálogo/mapa) e F-4 (roteiros do device): cache de
  metadados de pontos + rotas criadas, sem fotos, com warm-up em background; criação de rota segue
  somente online.
- Telas afetadas no front Blade: `entrepreneurs` (F-A/F-B), `detail`/aba empresa-turista (F-B),
  `admin/dashboard` e `admin/module` (F-C), `route-adaptation`/`route-result` (F-D),
  `home`/`catalog`/`detail`/`map`/`route-result` (F-E).
- Tema/cores do tenant aplicados via `tenant/config` (`F-10`), respeitando Bootstrap 4 + custom
  properties mínimas (`PLAN_FRONTEND.md` §2).
