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

26 KiB
Raw Blame History

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 emfrontend/src/pages/PropertiesPage.tsxpara stagger; resetar animação ao trocar de página (chave nokey` 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