sass-imobiliaria/specs/023-ux-melhorias-imoveis/tasks.md

426 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
description: "Tasks para a feature 023 - Melhorias UX/UI — Listagem de Imóveis"
---
# Tasks: Melhorias UX/UI — Listagem de Imóveis (023)
**Input**: Design documents de `specs/023-ux-melhorias-imoveis/`
**Prerequisites**: plan.md ✅ · spec.md ✅ · data-model.md ✅ · contracts/properties-api.md ✅ · auditoria: specs/022-ux-audit-imoveis/ux-audit.md ✅
**Sem migrations** — todos os campos usados já existem no modelo `Property`
---
## Format: `[ID] [P?] [Story?] Description — arquivo`
- **[P]**: Pode executar em paralelo (arquivo diferente, sem bloqueadores incompletos)
- **[Story]**: User story correspondente (US1US8)
- Arquivo exato indicado em cada task
- **Sprint** de cada fase indicado no cabeçalho
---
## Phase 1: Foundational — Backend (Bloqueador de testes de integração)
**Sprint**: Pré-sprint (deve preceder o início do Sprint 1)
**Purpose**: Adicionar `q` e `sort` na rota existente `GET /api/v1/properties`. Sem migration — campos `title`, `address`, `code`, `neighborhood_id`, `price`, `area_m2`, `created_at`, `is_featured` já existem. Este phase não tem dependências de frontend.
**⚠️ CRÍTICO**: As tasks T003T010 do Sprint 1 que dependem do backend (integração de busca textual) requerem T001 completo. As tasks de refactor de frontend (T004T007) podem ser iniciadas em paralelo com T001/T002.
- [ ] T001 Adicionar parâmetros `q` (busca ILIKE em `title`, `address`, `code`, `neighborhood.name` via `outerjoin` com `aliased(Neighborhood)`) e `sort` (whitelist com `sort_map`) na rota `GET /api/v1/properties` em `backend/app/routes/properties.py` — sanitização de `q`: `.strip()` + truncamento a 200 chars; `sort` com fallback para `created_at.desc()`
**Critérios de aceitação**:
- `GET /api/v1/properties?q=Jardins` retorna apenas imóveis com "Jardins" no título, endereço, código ou bairro
- `GET /api/v1/properties?sort=price_asc` retorna imóveis em ordem crescente de preço
- `GET /api/v1/properties?sort=invalido` retorna imóveis na ordem padrão (sem erro 400/500)
- `GET /api/v1/properties?q=<script>alert(1)</script>` não causa SQL injection nem 500
- [ ] T002 [P] Criar/atualizar testes pytest em `backend/tests/test_properties.py` para validar `q` (busca por título, por bairro, por código) e `sort` (price_asc retorna menor primeiro, area_desc retorna maior primeiro, valor desconhecido usa default) — fixture com ao menos 3 imóveis de preços distintos
**Critérios de aceitação**:
- `test_search_by_title_q`, `test_search_by_neighborhood_q`, `test_search_by_code_q` passam
- `test_sort_price_asc`, `test_sort_price_desc`, `test_sort_area_desc`, `test_sort_unknown_fallback` passam
- `pytest tests/test_properties.py -v` termina verde sem erros
**Checkpoint**: `curl "http://localhost:5000/api/v1/properties?q=test&sort=price_asc"` retorna 200 com `items` e `total`.
---
## Phase 2: Sprint 1 — Correções Críticas (P1)
**Sprint**: 1
**Purpose**: Resolver os 5 problemas 🔴 críticos identificados na auditoria: semântica HTML inválida (FR-001), carrossel inacessível em mobile (FR-002), ausência de tratamento de erro de rede (FR-003), layout fixo em tablets (FR-004) e campo de busca textual (FR-005 a FR-008).
**Independent Test (US1)**: Abrir `/imoveis` em mobile, navegar pelas fotos tocando em prev/next, simular falha de rede e verificar mensagem de erro. Inspecionar DOM e confirmar ausência de `<button>` dentro de `<a>`.
**Independent Test (US2)**: Digitar "Barra Funda" no campo de busca, verificar que URL muda para `/imoveis?q=Barra+Funda` e resultados são filtrados. Limpar busca e verificar retorno ao estado anterior.
---
### US1 — Correções Críticas de Usabilidade
- [ ] T003 [US1] Refatorar estrutura HTML do `frontend/src/components/PropertyRowCard.tsx` — substituir o `<Link>` que envolve toda a seção de informações por um overlay absoluto (`className="absolute inset-0" tabIndex={-1} aria-label="Ver detalhes: {title}"`); mover botões "Comparar" e "Entre em contato" para fora do `<Link>` com `relative z-index: 10`; envolver o card em `<article className="relative group ...">`**este refactor é pré-requisito para T005, T015 e T019**
**Critérios de aceitação**:
- Nenhum `<button>` aninhado dentro de `<a>` no DOM inspecionado
- Clicar no card (fora dos botões) navega para a página de detalhes
- Clicar em "Comparar" ou "Entre em contato" não dispara navegação
- Leitor de tela anuncia o link com `aria-label` correto
- [ ] T004 [US1] Corrigir visibilidade dos botões prev/next do carrossel em dispositivos touch em `frontend/src/components/PropertyRowCard.tsx` — trocar `opacity-0 group-hover:opacity-100` por `opacity-100 sm:opacity-0 sm:group-hover:opacity-100` nos botões de navegação do carrossel (visível sempre em mobile, hover-only em desktop)
**Critérios de aceitação**:
- Em viewport ≤640px, botões prev/next são visíveis sem toque/hover
- Em viewport ≥640px, botões prev/next aparecem apenas com hover no card
- Botões com apenas 1 foto ficam ocultos (`photos.length <= 1`)
- [ ] T005 [US1] Corrigir layout responsivo do card em `frontend/src/components/PropertyRowCard.tsx` — remover `h-[220px]` fixo do article e `w-[340px]` fixo da imagem; usar `flex flex-col sm:flex-row sm:h-[220px]` no article e `w-full h-48 sm:w-[280px] sm:h-full lg:w-[340px]` na div da imagem — garante que em tablets (7681023px) o conteúdo não seja truncado
**Critérios de aceitação**:
- Em viewport 768px, o card exibe título, endereço e stats sem corte de texto
- Em viewport 1024px, o card mantém layout horizontal com proporções corretas
- Em viewport 375px (mobile), o card exibe layout em coluna única sem overflow
- [ ] T006 [US1] Implementar tratamento de erro de rede em `frontend/src/pages/PropertiesPage.tsx` — adicionar `const [error, setError] = useState<string | null>(null)`; no bloco `catch` do `fetchProperties`, definir `setError('Não foi possível carregar os imóveis. Tente novamente.')` e limpar em nova tentativa; renderizar mensagem de erro com botão "Tentar novamente" que chama `fetchProperties()` no lugar da listagem vazia silenciosa
**Critérios de aceitação**:
- Com API inacessível (ex: container parado), mensagem de erro é exibida
- Botão "Tentar novamente" dispara novo request ao ser clicado
- Erro é limpo quando um request subsequente é bem-sucedido
- Skeleton de loading não aparece durante o estado de erro
- [ ] T007 [US1] Adicionar indicador visual de carregamento sutil em `frontend/src/pages/PropertiesPage.tsx` — aplicar `className={loading ? 'opacity-50 pointer-events-none' : 'opacity-100'}` com `transition-opacity duration-150` na div que envolve os cards; manter cards anteriores visíveis com opacidade reduzida ao invés de mostrar skeleton completo ao trocar filtros
**Critérios de aceitação**:
- Ao mudar qualquer filtro, os cards anteriores ficam com opacidade reduzida imediatamente (antes do response da API)
- Ao completar o request, opacidade volta a 100% com transição suave
- Cliques nos cards são bloqueados durante loading (`pointer-events-none`)
---
### US2 — Campo de Busca Textual
- [ ] T008 [P] [US2] Adicionar `q?: string` ao tipo `PropertyFilters` e criar tipo `SortOption = 'relevance' | 'price_asc' | 'price_desc' | 'area_desc' | 'newest'` com `sort?: SortOption` em `frontend/src/services/properties.ts` — incluir ambos como query params na chamada Axios
**Critérios de aceitação**:
- Compilação TypeScript sem erros após a alteração
- Chamada `getProperties({ q: 'Jardins', sort: 'price_asc' })` gera URL `?q=Jardins&sort=price_asc`
- `q` vazio ou undefined não adiciona `?q=` na URL (usar `params` do Axios com valores falsy omitidos)
- [ ] T009 [P] [US2] Criar `frontend/src/components/SearchBar.tsx` — input controlado com placeholder "Buscar por endereço, bairro ou código...", ícone de lupa, debounce de 400ms via `useEffect` + `setTimeout`, botão `×` visível quando há texto, limpa o campo e dispara `onSearch('')` ao clicar; props: `value: string`, `onSearch: (q: string) => void`
**Critérios de aceitação**:
- Digitar "Jard" não dispara chamada imediata; após 400ms de inatividade, `onSearch('Jard')` é chamado
- Botão `×` aparece quando `value.length > 0` e desaparece quando vazio
- Clicar em `×` chama `onSearch('')` e limpa o input
- Campo tem `role="search"` e `aria-label="Buscar imóveis"`
- [ ] T010 [US2] Integrar `SearchBar` em `frontend/src/pages/PropertiesPage.tsx` — posicionar acima do header de resultados (contador + sort), sincronizar com parâmetro `q` da URL via `useSearchParams`, resetar `page` para 1 ao mudar a busca, exibir estado vazio específico com sugestão de termos quando busca não retorna resultados
**Critérios de aceitação**:
- Digitar "Barra Funda" atualiza URL para `/imoveis?q=Barra+Funda` sem reload completo
- Compartilhar URL com `?q=Jardins` exibe resultados filtrados para o destinatário
- Limpar o campo remove `q` da URL e restaura listagem sem filtro textual
- Busca + filtros de sidebar funcionam combinados (AND lógico)
**Checkpoint Sprint 1**: Abrir `/imoveis`, inspecionar DOM sem `<button>` dentro de `<a>`, navegar fotos em mobile, testar busca por bairro, simular rede off e ver mensagem de erro.
---
## Phase 3: Sprint 2 — Alto Valor de Conversão (P2)
**Sprint**: 2
**Purpose**: Adicionar funcionalidades que aumentam diretamente a taxa de conversão: ordenação de resultados (FR-009 a FR-011), chips de filtros ativos (FR-012, FR-013), toggle Lista/Grade (FR-014, FR-015), estado vazio rico (FR-016) e hierarquia visual de CTAs (FR-017).
**Dependências**: T003 (refactor do card) deve estar completo antes de T019 (CTAs). T008 (PropertyFilters) deve estar completo antes de T011. T015 (PropertyGridCard) deve estar completo antes de T019 aplicar a este.
---
### US3 — Ordenação de Resultados
- [ ] T011 [US3] Adicionar seletor de ordenação no header de resultados em `frontend/src/pages/PropertiesPage.tsx``<select>` com 5 opções mapeadas para `SortOption`, ao lado do contador "X imóveis encontrados"; sincronizar `sort` com URL via `useSearchParams`; resetar `page` para 1 ao mudar ordenação; manter `sort` ao trocar de página
**Critérios de aceitação**:
- Selecionar "Menor preço" atualiza URL para `?sort=price_asc` e reordena a listagem
- Navegar para a página 2 com `sort=price_asc` mantém a ordenação na nova página
- Compartilhar URL com `?sort=newest` exibe mesma ordenação para o destinatário
- Opção "Relevância" é a default quando `sort` está ausente na URL
---
### US4 — Chips de Filtros Ativos
- [ ] T012 [P] [US4] Criar `frontend/src/components/ActiveFiltersBar.tsx` — recebe `filters: PropertyFilters` e `catalogData` (tipos, cidades, bairros); deriva array de `ActiveFilterChip[]` com `key`, `label` legível e `onRemove: () => void`; renderiza chips com botão `×` usando `aria-label="Remover filtro {label}"`; exibe botão "Limpar tudo" apenas quando `chips.length >= 2`; não renderiza nada quando `chips.length === 0`
**Critérios de aceitação**:
- Com filtros `listing_type=aluguel` + `city_id=1` + `bedrooms_min=2`, renderiza 3 chips com labels legíveis
- Clicar no `×` do chip "São Paulo" remove apenas `city_id` dos filtros e dispara `onFilterChange`
- Botão "Limpar tudo" aparece com ≥2 chips e remove todos ao clicar
- Com zero filtros ativos, o componente não renderiza nenhum elemento no DOM
- [ ] T013 [US4] Integrar `ActiveFiltersBar` em `frontend/src/pages/PropertiesPage.tsx` — posicionar abaixo de `SearchBar`, acima do primeiro card; passar `filters` atual e callbacks de remoção individual por chave de filtro (`onRemove(key) => setFilters(prev => omit(prev, key))`)
**Critérios de aceitação**:
- Aplicar filtro de tipo + cidade exibe chips correspondentes acima dos resultados
- Remover chip via `×` atualiza a listagem sem apagar outros filtros ativos
- Chips desaparecem quando todos os filtros são removidos via "Limpar tudo"
---
### US5 — Toggle de Visualização Lista/Grade
- [ ] T014 [P] [US5] Criar `frontend/src/components/PropertyGridCard.tsx` — card vertical com foto em destaque (aspectRatio 4/3, `object-cover`), título, preço, badges básicos (quartos/área/vagas), `<Link to={/imoveis/${slug}}>` como overlay absoluto (`tabIndex={-1}`), botão "Ver detalhes" como CTA primário visível; sem botões "Comparar" e "Entre em contato" (modo grade prioriza descoberta)
**Critérios de aceitação**:
- Card renderiza foto, título e preço sem truncamento em qualquer largura de coluna
- Clicar no card (fora do botão) navega para a página de detalhes
- Clicar em "Ver detalhes" navega para a página de detalhes
- Sem `<button>` aninhado em `<a>` no DOM
- [ ] T015 [US5] Adicionar toggle Lista/Grade no header de `frontend/src/pages/PropertiesPage.tsx` — estado `viewMode: ViewMode` inicializado de `localStorage.getItem('imoveis_view_mode') ?? 'list'`; dois botões de toggle com ícones (≡ Lista / ⊞ Grade) com `aria-pressed`; grid responsivo quando grade (`grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4`) vs flex-col quando lista; persistir no `localStorage` ao mudar
**Critérios de aceitação**:
- Clicar em "Grade" alterna layout para grid de 13 colunas responsivo com `PropertyGridCard`
- Recarregar a página mantém o modo de visualização selecionado (localStorage)
- Botão ativo recebe indicação visual distinta (`aria-pressed="true"`)
- Navegação para detalhe funciona em ambos os modos
---
### US6 — Estado Vazio com Sugestões
- [ ] T016 [P] [US6] Criar `frontend/src/components/EmptyStateWithSuggestions.tsx` — recebe `currentFilters: PropertyFilters` e `onApplySuggestion: (filters: PropertyFilters) => void`; exibe mensagem "Nenhum imóvel encontrado" + lista de sugestões acionáveis (ex: remover filtro de bairro, ampliar faixa de preço, reduzir mínimo de quartos), cada sugestão com contagem de imóveis seria encontrada (recebida via prop `suggestions: EmptyStateSuggestion[]`); botão "Limpar todos os filtros"
**Critérios de aceitação**:
- Exibe ao menos 3 sugestões quando há filtros ativos
- Clicar em sugestão chama `onApplySuggestion` com filtros relaxados e atualiza listagem
- Botão "Limpar todos os filtros" remove todos os filtros e retorna resultados
- Contagem de imóveis por sugestão é exibida (ex: "→ 12 imóveis disponíveis")
- [ ] T017 [US6] Integrar `EmptyStateWithSuggestions` em `frontend/src/pages/PropertiesPage.tsx` — quando `result.total === 0` e `!loading`, fazer 3 requests paralelos (`Promise.all`) com filtros relaxados (sem `neighborhood_id`, sem `bedrooms_min`, sem `price_max`) para calcular contagens; passar `suggestions` para o componente; substituir o estado vazio simples atual
**Critérios de aceitação**:
- Com filtros impossíveis (ex: `bedrooms_min=10`), estado vazio mostra sugestões com contagens reais
- Requests de sugestões são paralelos (não sequenciais), sem bloquear a UI
- Clicar numa sugestão atualiza os filtros ativos e exibe os resultados correspondentes
- Quando não há filtros ativos e o resultado é vazio, exibe mensagem genérica sem sugestões
---
### US7 — Hierarquia Visual de CTAs no Card
- [ ] T018 [US7] Atualizar hierarquia visual dos CTAs em `frontend/src/components/PropertyRowCard.tsx` — "Ver detalhes" como `<Link>` com estilo primário (fundo `var(--color-brand)`, texto branco); "Entre em contato" como `<button>` com estilo outline (borda `var(--color-brand)`, background transparente); "Comparar" como `<button>` com estilo ghost (sem borda, apenas texto muted com hover sutil); manter todos fora do `<Link>` overlay (depende de T003)
**Critérios de aceitação**:
- "Ver detalhes" tem fundo colorido e destaque visual primário
- "Entre em contato" tem borda colorida sem fundo (outline)
- "Comparar" tem aparência discreta sem borda (ghost/minimal)
- Hierarquia mantida em viewport mobile (375px)
- Nenhum `<button>` dentro de `<a>` no DOM
**Checkpoint Sprint 2**: Aplicar filtros de tipo + cidade + quartos, verificar chips aparecem. Selecionar ordenação por preço. Alternar para grade. Aplicar filtro impossível e verificar sugestões. Confirmar hierarquia visual dos CTAs.
---
## Phase 4: Sprint 3 — Refinamentos de Qualidade (P3)
**Sprint**: 3
**Purpose**: Polimento percebido que aumenta a sensação de qualidade do produto sem bloquear fluxos de uso: animações (FR-018), indicador de paginação (FR-019), scroll-to-top (FR-020), badges de status (FR-021, FR-022), teclado no carrossel (FR-023), paginação no topo (FR-024), skeleton no sidebar (FR-025).
**Independent Test (US8)**: Navegar para página 2, verificar "Exibindo XY de Z imóveis"; pressionar Tab no carrossel e usar setas para navegar; verificar badge "Destaque" em imóvel com `is_featured=true`.
---
### US8 — Refinamentos de Qualidade
- [ ] T019 [US8] Adicionar keyframe `@keyframes fade-in-up` em `frontend/src/index.css` (translateY de 8px→0, opacity 0→1, duration 300ms ease-out) e aplicar `style={{ animationDelay: \`${index * 40}ms\` }}` nos cards mapeados em `frontend/src/pages/PropertiesPage.tsx` para stagger; resetar animação ao trocar de página (chave no `key` do item)
**Critérios de aceitação**:
- Cards entram com animação sutil ao carregar nova página
- Stagger visível entre cards consecutivos (~40ms de diferença)
- Animação não ocorre durante loading (cards com opacidade reduzida) — apenas após novo resultado
- Sem `prefers-reduced-motion` override (adicionar `@media (prefers-reduced-motion: reduce)` sem animação)
- [ ] T020 [P] [US8] Adicionar indicador de posição "Exibindo XY de Z imóveis" em `frontend/src/pages/PropertiesPage.tsx` — calcular `from = (page - 1) * perPage + 1`, `to = Math.min(page * perPage, total)`; renderizar próximo ao contador de resultados ou acima da paginação inferior
**Critérios de aceitação**:
- Na página 1 com 16 por página e 45 total: exibe "Exibindo 116 de 45 imóveis"
- Na página 3: exibe "Exibindo 3345 de 45 imóveis"
- Não exibir quando `total === 0` (estado vazio)
- [ ] T021 [P] [US8] Criar `frontend/src/components/ScrollToTopButton.tsx` — botão flutuante fixo (`fixed bottom-6 right-6`), aparece quando `scrollY > 400` via `useEffect` com listener de `scroll`, chama `window.scrollTo({ top: 0, behavior: 'smooth' })` ao clicar; integrar em `frontend/src/pages/PropertiesPage.tsx` como filho direto da página
**Critérios de aceitação**:
- Botão fica oculto antes de 400px de scroll e aparece após esse limiar
- Clicar no botão rola suavemente para o topo
- Botão tem `aria-label="Voltar ao topo"` para acessibilidade
- Listener de scroll é removido no cleanup do `useEffect` (sem leak)
- [ ] T022 [US8] Adicionar badges "Destaque" e "Novo" sobrepostos à foto em `frontend/src/components/PropertyRowCard.tsx` e `frontend/src/components/PropertyGridCard.tsx` — badge "Destaque" quando `property.is_featured === true` (fundo âmbar, `⭐ Destaque`); badge "Novo" quando `created_at` for de até 7 dias atrás — calculado no frontend: `Date.now() - new Date(created_at).getTime() < 7 * 24 * 60 * 60 * 1000`; posicionar `absolute top-2 left-2` na div da foto
**Critérios de aceitação**:
- Imóvel com `is_featured=true` exibe badge "⭐ Destaque" na foto
- Imóvel com `created_at` de ontem exibe badge "Novo" na foto
- Imóvel com `created_at` de 8 dias atrás não exibe badge "Novo"
- Ambos os badges podem coexistir no mesmo card
- [ ] T023 [US8] Adicionar navegação por teclado no carrossel de `frontend/src/components/PropertyRowCard.tsx` — botões prev/next devem ser focáveis via Tab; ao focar qualquer botão do carrossel, adicionar `onKeyDown` que responde a `ArrowLeft` (prev) e `ArrowRight` (next); `aria-label="Foto anterior"` / `"Próxima foto"` nos botões
**Critérios de aceitação**:
- Tab navega para os botões prev/next do carrossel
- Pressionar ArrowRight no botão next avança o slide
- Pressionar ArrowLeft no botão prev retrocede o slide
- Botões com 1 única foto ficam com `aria-disabled="true"` e não respondem a teclado
- [ ] T024 [P] [US8] Adicionar paginação duplicada no topo da listagem em `frontend/src/pages/PropertiesPage.tsx` — renderizar o mesmo componente de paginação (já existente) acima do primeiro card, com `aria-label="Paginação superior"`; visível apenas quando `result.pages > 1`
**Critérios de aceitação**:
- Com mais de 1 página de resultados, paginação aparece no topo E no rodapé
- Com 1 página apenas, apenas o rodapé é exibido
- Ambas as paginações atualizam a página ao mesmo tempo (estado compartilhado)
- [ ] T025 [P] [US8] Adicionar skeleton de carregamento no `frontend/src/components/FilterSidebar.tsx` — exibir placeholders animados (`animate-pulse bg-surface rounded`) no lugar dos filtros de tipo, cidade, bairro e comodidades enquanto `catalogLoading === true`; a listagem de imóveis continua carregando independentemente
**Critérios de aceitação**:
- Enquanto `catalogLoading` for true, skeleton é exibido no sidebar sem bloquear a listagem
- Ao completar o carregamento, skeleton é substituído pelos filtros reais sem flash
- Skeleton tem mesma altura aproximada dos filtros para evitar CLS
**Checkpoint Sprint 3**: Navegar para página 2 e verificar indicador de posição. Rolar 400px e verificar botão flutuante. Verificar badge em imóvel com `is_featured=true`. Testar Tab + setas no carrossel.
---
## Phase 5: Polish & Verificação Final
**Purpose**: Validação cruzada de semântica HTML, acessibilidade, TypeScript e testes backend.
- [ ] T026 Inspecionar DOM de `/imoveis` no browser e verificar ausência de `<button>` dentro de `<a>` em todos os cards (lista e grade) — corrigir qualquer instância remanescente em `frontend/src/components/PropertyRowCard.tsx` ou `frontend/src/components/PropertyGridCard.tsx`
**Critérios de aceitação**:
- DevTools → Elements: nenhum seletor `a button`, `a [role=button]` encontrado
- Validação HTML5 sem erros de aninhamento inválido
- [ ] T027 Executar testes backend e verificar build TypeScript sem erros — `docker-compose exec backend uv run pytest tests/test_properties.py -v` deve terminar verde; `docker-compose exec frontend npx tsc --noEmit` deve terminar sem erros
**Critérios de aceitação**:
- Todos os testes pytest de `test_properties.py` passam
- Compilação TypeScript sem erros de tipo
- Nenhum `console.error` no browser ao carregar `/imoveis`
---
## Dependency Graph
```
T001 (backend q+sort)
└─► T002 (testes backend)
└─► T010 (integração SearchBar — valida endpoint)
T003 (refactor HTML card)
└─► T004 (carrossel mobile — mesmo arquivo)
└─► T005 (layout tablet — mesmo arquivo)
└─► T018 (CTAs — reestrutura botões)
└─► T022 (badges — adiciona na foto já reestruturada)
└─► T023 (teclado carrossel — botões reestruturados)
T008 (PropertyFilters tipos)
└─► T009 (SearchBar usa onSearch callback)
└─► T010 (PropertiesPage usa q no state)
└─► T011 (PropertiesPage usa sort no state)
└─► T012 (ActiveFiltersBar usa PropertyFilters)
└─► T016 (EmptyStateWithSuggestions usa PropertyFilters)
T014 (PropertyGridCard — novo componente)
└─► T015 (toggle grade renderiza PropertyGridCard)
└─► T022 (badges adicionados em PropertyGridCard)
T006 (error state PropertiesPage)
└─► T007 (opacity loading — mesmo arquivo, mesma sessão)
└─► T010 (integração SearchBar — mesmo arquivo)
└─► T011 (seletor sort — mesmo arquivo)
└─► T013 (integra ActiveFiltersBar — mesmo arquivo)
└─► T015 (toggle grade — mesmo arquivo)
└─► T017 (integra EmptyState — mesmo arquivo)
└─► T019 (animação — mesmo arquivo)
└─► T020 (indicador posição — mesmo arquivo)
└─► T024 (paginação top — mesmo arquivo)
```
---
## Parallel Execution Examples
### Sprint 1 — Paralelo possível
```
Thread A: T001 → T002
Thread B: T003 → T004 → T005
Thread C: T008 → T009
Thread D: T006 → T007
```
→ Após threads B e C concluídos: T010 (integra SearchBar em PropertiesPage com q sincronizado)
### Sprint 2 — Paralelo possível
```
Thread A: T011 (sort selector em PropertiesPage)
Thread B: T012 (ActiveFiltersBar — novo arquivo)
Thread C: T014 (PropertyGridCard — novo arquivo)
Thread D: T016 (EmptyStateWithSuggestions — novo arquivo)
```
→ Após thread B: T013 (integra ActiveFiltersBar em PropertiesPage)
→ Após thread C: T015 (toggle grade em PropertiesPage)
→ Após thread D: T017 (integra EmptyState em PropertiesPage)
→ Após T003 completo: T018 (CTAs em PropertyRowCard)
### Sprint 3 — Paralelo possível
```
Thread A: T019 (animações — PropertiesPage + index.css)
Thread B: T020 (indicador posição — PropertiesPage)
Thread C: T021 (ScrollToTopButton — novo arquivo)
Thread D: T024 (paginação top — PropertiesPage)
Thread E: T025 (skeleton sidebar — FilterSidebar)
```
→ Após T003+T014: T022 (badges em ambos os cards)
→ Após T003: T023 (teclado carrossel em PropertyRowCard)
---
## Implementation Strategy
### MVP Scope (Sprint 1 apenas)
Para uma entrega incremental mínima que resolve os problemas críticos bloqueadores de conversão:
- **T001** + **T002**: Backend com `q` e `sort`
- **T003** + **T004** + **T005**: Card sem HTML inválido e funcional em mobile/tablet
- **T006** + **T007**: Tratamento de erro e feedback de loading
- **T008** + **T009** + **T010**: Campo de busca textual funcional
Resultado: `/imoveis` sem erros críticos de HTML, funcional em mobile/tablet, com busca textual e tratamento de erros.
### Sprint 2 — Funcionalidades de Conversão
Adicionar T011 (ordenação), T012T013 (chips), T014T015 (grade), T016T017 (empty state rico), T018 (CTAs).
### Sprint 3 — Polimento
Adicionar T019T025 (animações, badges, teclado, scroll-to-top, paginação dupla, skeleton sidebar).
---
## Summary
| Métrica | Valor |
|---|---|
| Total de tasks | 27 |
| Sprint 1 (P1 — crítico) | T001T010 (10 tasks) |
| Sprint 2 (P2 — alto valor) | T011T018 (8 tasks) |
| Sprint 3 (P3 — refinamentos) | T019T025 (7 tasks) |
| Polish | T026T027 (2 tasks) |
| Tasks backend | T001, T002 (2 tasks) |
| Tasks frontend | T003T025 (23 tasks) |
| Tasks de teste | T002 (pytest backend) |
| Tasks paralelizáveis [P] | T002, T008, T009, T012, T014, T016, T020, T021, T024, T025 |
| Novos componentes | SearchBar, PropertyGridCard, ActiveFiltersBar, EmptyStateWithSuggestions, ScrollToTopButton |
| Arquivos modificados | PropertyRowCard, PropertiesPage, FilterSidebar, services/properties.ts, index.css |
| Migrations de banco | Nenhuma |