24 KiB
Tasks: Filtro de Busca Avançada — FilterSidebar
Input: Design documents from /specs/024-filtro-busca-avancada/
Prerequisites: plan.md ✅ · spec.md ✅ · data-model.md ✅ · contracts/api-catalog-enhancements.md ✅
Branch: 024-filtro-busca-avancada
Format: [ID] [P?] [Story?] Description
- [P]: pode ser executada em paralelo (arquivos distintos, sem dependência de tarefa incompleta)
- [Story]: a qual user story pertence (
US1,US2,US3) - Caminhos exatos incluídos em cada tarefa
Phase 1: Foundational — Backend property_count
Purpose: Enriquecer os três endpoints de catálogo com property_count calculado via COUNT dinâmico (SQLAlchemy subquery). Sem migration — campo somente-leitura calculado em tempo de execução. Este é o pré-requisito de US3; US1 e US2 podem prosseguir em paralelo independentemente desta fase.
⚠️ BLOQUEANTE para US3: US3 não pode ser iniciada até T004 estar completo.
-
T001 Adicionar
property_count: int = 0às classesPropertyTypeOut,CityOuteNeighborhoodOutembackend/app/schemas/catalog.py- Done: Os três schemas Pydantic possuem o campo
property_count: int = 0como atributo opcional de saída;PropertyTypeOut.model_rebuild()continua presente após a mudança; testes de serialização passam com o campo default.
- Done: Os três schemas Pydantic possuem o campo
-
T002 Atualizar
list_property_types()embackend/app/routes/catalog.pypara calcularproperty_countpor subtype via subquery SQLAlchemy (func.count + outerjoin + group_by) e injetar no dict serializado- Detalhes: importar
funcdesqlalchemyePropertydeapp.models.property; query plana de subtypes (parent_id IS NOT NULL) comouterjoin(Property, (Property.subtype_id == PropertyType.id) & (Property.is_active.is_(True)))+group_by(PropertyType.id); construircount_map: dict[int, int]; substituir ojsonifyatual porserialize_category()que injetaproperty_countem cada subtype viacount_map.get(sub["id"], 0); tipos pai mantêmproperty_count: 0. - Done:
GET /api/v1/property-typesretorna cada subtype comproperty_count >= 0; tipos pai retornamproperty_count: 0; resposta é válida com ou sem imóveis ativos.
- Detalhes: importar
-
T003 [P] Atualizar
list_cities()embackend/app/routes/locations.pypara calcularproperty_countviaouterjoin(Property) + func.count + group_by- Detalhes: importar
funcdesqlalchemyePropertydeapp.models.property; substituirCity.query.order_by(...)pordb.session.query(City, func.count(Property.id).label("cnt")).outerjoin(Property, (Property.city_id == City.id) & (Property.is_active.is_(True))).group_by(City.id).order_by(City.state, City.name).all(); retornar{**CityOut.model_validate(city).model_dump(), "property_count": cnt}. - Done:
GET /api/v1/citiesretorna cada cidade comproperty_count >= 0; cidades sem imóveis retornam0; ordenaçãostate ASC, name ASCmantida.
- Detalhes: importar
-
T004 [P] Atualizar
list_neighborhoods()embackend/app/routes/locations.pypara calcularproperty_countviaouterjoin(Property) + func.count + group_by- Detalhes: substituir
Neighborhood.querypordb.session.query(Neighborhood, func.count(Property.id).label("cnt")).outerjoin(Property, (Property.neighborhood_id == Neighborhood.id) & (Property.is_active.is_(True))).group_by(Neighborhood.id); preservar filtro?city_idexistente aplicado antes de.all(); retornar{**NeighborhoodOut.model_validate(n).model_dump(), "property_count": cnt}. - Done:
GET /api/v1/neighborhoodseGET /api/v1/neighborhoods?city_id=Nretornam bairros comproperty_count >= 0; filtrocity_idainda funciona.
- Detalhes: substituir
Checkpoint Phase 1: Os três endpoints retornam property_count correto. Validar manualmente com curl http://localhost:5000/api/v1/cities e confirmar campo presente na resposta JSON.
Phase 2: Foundational — Frontend Types
Purpose: Espelhar o campo property_count do backend nos tipos TypeScript. Backward-compatible (campo opcional). Pré-requisito de US3 no frontend.
- T005 [P] Adicionar
property_count?: numberàs interfacesPropertyType,CityeNeighborhoodemfrontend/src/types/catalog.ts- Detalhes: campo opcional (
?) para backward-compatibility — componentes que não usam o campo continuam compilando sem alteração. - Done:
frontend/src/types/catalog.tscompila sem erros (tsc --noEmit); as três interfaces possuemproperty_count?: number; nenhum componente existente quebra.
- Detalhes: campo opcional (
Checkpoint Phase 2: tsc --noEmit passa. T005 pode ser executada em paralelo com a Phase 1 inteira.
Phase 3: US2 — Estado Inicial Controlado das Seções (Priority: P1)
Goal: Ao carregar /imoveis, apenas a seção "Preço" está expandida. Seções com filtros ativos na URL são auto-expandidas. Estado não persistido entre sessões.
Independent Test: Carregar /imoveis sem parâmetros de URL e verificar que somente a seção "Preço" está expandida; todas as demais (Imobiliária, Localização, Tipo, Quartos, Área, Comodidades) estão colapsadas.
Dependências: nenhuma tarefa anterior é bloqueante para US2.
-
T006 [US2] Declarar o tipo
SectionKeylocalmente emfrontend/src/components/FilterSidebar.tsx- Detalhes: adicionar antes da definição de
Section; valores:'imobiliaria' | 'localizacao' | 'tipo' | 'preco' | 'quartos' | 'area' | 'comodidades'; não exportar. - Done: tipo
SectionKeydeclarado no arquivo; sem erro de compilação.
- Detalhes: adicionar antes da definição de
-
T007 [US2] Implementar a função pura
initOpenSections(filters: PropertyFilters): Record<SectionKey, boolean>localmente emfrontend/src/components/FilterSidebar.tsx- Detalhes:
preco: truesempre;imobiliaria: filters.imobiliaria_id != null;localizacao: filters.city_id != null || filters.neighborhood_id != null;tipo: filters.subtype_id != null;quartos: filters.bedrooms_min != null || filters.bathrooms_min != null || filters.parking_min != null;area: filters.area_min != null || filters.area_max != null;comodidades: (filters.amenity_ids?.length ?? 0) > 0. - Done: função declarada antes do componente; retorna objeto com todas as 7 chaves;
precosempretrue.
- Detalhes:
-
T008 [US2] Converter o sub-componente
Sectionpara suportar modo controlled emfrontend/src/components/FilterSidebar.tsx- Detalhes: adicionar props opcionais
open?: booleaneonToggle?: () => voidà interface do componente; quandoopen !== undefined, usaropencomo state do accordion e chamaronToggleno click do botão; quandoopen === undefined, manter comportamento atual viauseState(defaultOpen)(fallback uncontrolled); nenhum caller externo é quebrado. - Done:
SectionaceitaopeneonToggleopcionais; modo uncontrolled continua funcionando; modo controlled expande/colapsa corretamente ao passaropen={true/false}+onToggle.
- Detalhes: adicionar props opcionais
-
T009 [US2] Adicionar estado
openSectionse helpertoggleSectionno componente principalFilterSidebaremfrontend/src/components/FilterSidebar.tsx- Detalhes:
const [openSections, setOpenSections] = useState<Record<SectionKey, boolean>>(() => initOpenSections(filters));function toggleSection(key: SectionKey) { setOpenSections(prev => ({ ...prev, [key]: !prev[key] })) }; adicionar tambémfunction expandSection(key: SectionKey) { setOpenSections(prev => ({ ...prev, [key]: true })) }(usado por US1 na T015). - Done: estado
openSectionsinicializado corretamente;toggleSectionalterna o valor da chave;expandSectiongarantetruesem alterar demais.
- Detalhes:
-
T010 [US2] Conectar todas as seções ao estado
openSectionsemfrontend/src/components/FilterSidebar.tsx- Detalhes: cada
<Section>deve receberopen={openSections['<key>']}eonToggle={() => toggleSection('<key>')}— aplicar nas seções: Imobiliária (imobiliaria), Localização (localizacao), Tipo de imóvel (tipo), Preço (preco), Quartos e vagas (quartos), Área (area), Comodidades (comodidades); removerdefaultOpenprops ondeopené passado. - Done: todas as seções do sidebar são controlled; ao carregar sem URL params, apenas "Preço" está aberta; seções com filtros ativos na URL são abertas automaticamente; toggle manual funciona em cada seção.
- Detalhes: cada
Checkpoint US2: Carregar /imoveis — somente seção "Preço" expandida ✓. Carregar /imoveis?city_id=1 — seções "Preço" e "Localização" expandidas ✓. Toggle manual colapsa/expande corretamente ✓.
Phase 4: US1 — Campo de Busca Cross-Categoria (Priority: P1)
Goal: Campo de busca no topo do sidebar que filtra todos os itens do catálogo instantaneamente (debounce 200 ms), exibe sugestões agrupadas por categoria com highlight, seleciona ao clicar ou pressionar Enter, expande a seção relevante e limpa o campo.
Independent Test: Digitar "Copa" no campo de busca do sidebar e verificar que aparece sugestão "Copacabana" sob o grupo "Bairro"; clicar na sugestão e confirmar que o filtro de bairro é aplicado, o campo é limpo e a seção "Localização" é expandida.
Dependências: T008 e T009 (US2) devem estar completos para que expandSection esteja disponível.
-
T011 [US1] Declarar a interface
FilterSuggestione implementar a funçãocomputeSuggestions()localmente emfrontend/src/components/FilterSidebar.tsx- Detalhes: interface
FilterSuggestion { category: string; sectionKey: SectionKey; label: string; filterKey: keyof PropertyFilters; value: number | string | undefined; isAmenity?: boolean; amenityId?: number }; funçãocomputeSuggestions(query: string, propertyTypes: PropertyType[], cities: City[], neighborhoods: Neighborhood[], amenities: Amenity[]): FilterSuggestion[]; normalização:text.normalize('NFD').replace(/\p{Mn}/gu, '').toLowerCase(); varrerpropertyTypes[*].subtypes→ categoria"Tipo de imóvel",sectionKey: 'tipo',filterKey: 'subtype_id';cities→"Cidade",sectionKey: 'localizacao',filterKey: 'city_id';neighborhoods→"Bairro",sectionKey: 'localizacao',filterKey: 'neighborhood_id';amenities→"Comodidade",sectionKey: 'comodidades',isAmenity: true; retornar array vazio sequery.trim() === ''. - Done:
computeSuggestions('copa', [...], [...], [...], [...])retorna ao menos uma entrada comlabel: 'Copacabana'ecategory: 'Bairro'; busca é case-insensitive e ignora acentos; query vazia retorna[].
- Detalhes: interface
-
T012 [US1] Criar sub-componente local
SidebarSearchInputemfrontend/src/components/FilterSidebar.tsx- Detalhes: props
{ value: string; onChange: (v: string) => void; disabled?: boolean; onKeyDown?: (e: React.KeyboardEvent) => void }; renderiza<input>com placeholder"Buscar filtro…", ícone de lupa (SVG inline), classes Tailwind:w-full text-xs; quandodisabled:cursor-not-allowed opacity-50;aria-label="Buscar filtro". - Done: campo renderiza com placeholder correto; estado
disabledvisualmente diferenciado; sem dependências externas.
- Detalhes: props
-
T013 [US1] Criar sub-componente local
SuggestionListcom navegação por teclado emfrontend/src/components/FilterSidebar.tsx- Detalhes: props
{ suggestions: FilterSuggestion[]; onSelect: (s: FilterSuggestion) => void; activeIndex: number }; agrupar porcategorye renderizar cabeçalhos de grupo (text-[10px] font-semibold text-textTertiary uppercase); cada item:<button>comdata-index, texto com highlight do termo buscado (fragmento<mark>combg-brand/20 text-brand); item ativo recebe classe de destaquebg-surface; quandosuggestions.length === 0e o caller passou query não-vazia, exibir"Nenhum filtro encontrado"em texto terciário; renderizado inline sob o campo (não é popup/portal). - Done: grupos renderizados com cabeçalho; clique em item chama
onSelect; item comactiveIndexvisualmente destacado; mensagem de "sem resultados" visível quando array vazio mas busca ativa.
- Detalhes: props
-
T014 [US1] Adicionar estados
filterSearch,searchQueryeactiveIndexcom debounce 200 ms no componenteFilterSidebaremfrontend/src/components/FilterSidebar.tsx- Detalhes:
const [filterSearch, setFilterSearch] = useState('');const [searchQuery, setSearchQuery] = useState('');const [activeIndex, setActiveIndex] = useState(-1);useEffect(() => { const t = setTimeout(() => { setSearchQuery(filterSearch); setActiveIndex(-1) }, 200); return () => clearTimeout(t) }, [filterSearch]);const suggestions = useMemo(() => computeSuggestions(searchQuery, propertyTypes, cities, neighborhoods, amenities), [searchQuery, propertyTypes, cities, neighborhoods, amenities]). - Done: alterar
filterSearchsó atualizasearchQueryapós 200 ms;suggestionsrecomputa quandosearchQuerymuda;activeIndexreseta ao mudar query.
- Detalhes:
-
T015 [US1] Implementar a função
handleSuggestionSelecte o handler de tecladohandleSearchKeyDownemfrontend/src/components/FilterSidebar.tsx- Detalhes:
handleSuggestionSelect(s: FilterSuggestion): ses.isAmenity, chamartoggleAmenity(s.amenityId!); senão, chamarset({ [s.filterKey]: s.value }); depois:expandSection(s.sectionKey);setFilterSearch('');handleSearchKeyDown(e: React.KeyboardEvent):ArrowDown→setActiveIndex(i => Math.min(i + 1, suggestions.length - 1));ArrowUp→setActiveIndex(i => Math.max(i - 1, -1));Enter→ seactiveIndex >= 0, selecionarsuggestions[activeIndex];Escape→setFilterSearch(''). - Done: clicar em sugestão aplica filtro + expande seção + limpa campo; navegação ↑↓ move destaque; Enter seleciona; Escape limpa; "Copacabana" selecionado aplica
city_idouneighborhood_idconforme categoria.
- Detalhes:
-
T016 [US1] Renderizar
SidebarSearchInputeSuggestionListno topo do componenteFilterSidebaremfrontend/src/components/FilterSidebar.tsx- Detalhes: inserir
<SidebarSearchInput>após o bloco "Tipo de negócio" (selector Venda/Aluguel/Todos) e antes do primeiro<div className="h-px bg-borderSubtle">que divide as seções; abaixo, renderizar condicionalmente{filterSearch.length > 0 && <SuggestionList suggestions={suggestions} onSelect={handleSuggestionSelect} activeIndex={activeIndex} />}; passardisabled={catalogLoading}eonKeyDown={handleSearchKeyDown}aoSidebarSearchInput; quandoSuggestionListestá visível, as seções accordion continuam montadas (não desmontadas). - Done: campo visível no topo do sidebar; sugestões aparecem ao digitar; desabilitado quando
catalogLoading=true; sugestões somem ao limpar campo ou pressionar Escape.
- Detalhes: inserir
Checkpoint US1: Digitar "apar" → sugestão "Apartamento" sob "Tipo de imóvel" aparece em < 50 ms ✓. Clicar → filtro aplicado, campo limpo, seção "Tipo de imóvel" expandida ✓. Digitar "xxxxxxxxx" → "Nenhum filtro encontrado" ✓. Teclado ↑↓Enter funciona ✓.
Phase 5: US3 — Truncamento, Popularidade e Badge (Priority: P2)
Goal: Cada seção mostra os 5 itens mais populares (por property_count DESC). Botão "Ver mais (N)" expande; "Ver menos" retrai. Os 3 mais populares exibem badge "Popular". Itens com filtro ativo são sempre visíveis.
Independent Test: Abrir seção "Bairros" no sidebar — apenas os 5 bairros com mais imóveis exibidos; badge "Popular" no primeiro; clicar "Ver mais" → todos visíveis; clicar "Ver menos" → volta a 5.
Dependências: T001–T005 (property_count no backend e frontend types) + T008–T010 (Section controlled, para integração harmoniosa).
-
T017 [US3] Criar sub-componente local
PopularBadgeemfrontend/src/components/FilterSidebar.tsx- Detalhes: sem props além de children opcionais; renderiza
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold bg-brand/20 text-brand leading-none ml-1.5">Popular</span>. - Done: componente renderiza o badge com tokens de design corretos; sem dependências externas.
- Detalhes: sem props além de children opcionais; renderiza
-
T018 [US3] Criar sub-componente local
TruncatedFilterList<T>emfrontend/src/components/FilterSidebar.tsx- Detalhes: props genéricos
{ items: T[]; selectedId?: number | null; renderItem: (item: T, isPopular: boolean) => React.ReactNode; getId: (item: T) => number; getCount: (item: T) => number; topN?: number }; lógica: ordenar porgetCount(item) DESC→sorted; se itemselectedIdnão está nos toptopN(padrão 5), promovê-lo para início desorted(sem alterar restante);const [expanded, setExpanded] = useState(false); exibirexpanded ? sorted : sorted.slice(0, topN);isPopular = index < 3(após promoção, se o item promovido não era top-3, não recebe badge); botão "Ver mais (N)" / "Ver menos" somente quandosorted.length > topN; ao clicar "Ver menos", não fazer scroll automático. - Done: com 8 itens exibe 5; botão "Ver mais (3)" aparece; clicar expande para 8; "Ver menos" recolhe; com 3 itens, botão não aparece; item selecionado fora do top-5 é promovido e visível sem clicar "Ver mais"; badge "Popular" nos índices 0, 1, 2 da lista ordenada.
- Detalhes: props genéricos
-
T019 [US3] Aplicar
TruncatedFilterListna seção "Tipo de imóvel" para os subtypes emfrontend/src/components/FilterSidebar.tsx- Detalhes: para cada categoria (
propertyType), substituir o map de subtypes atual por<TruncatedFilterList items={propertyType.subtypes} selectedId={filters.subtype_id ?? null} getId={s => s.id} getCount={s => s.property_count ?? 0} renderItem={(s, isPopular) => <...chip existente...>{isPopular && <PopularBadge />}</...>} />; ordenação ocorre dentro deTruncatedFilterList. - Done: seção "Tipo de imóvel" exibe top-5 subtypes por property_count; badge "Popular" nos 3 primeiros; "Ver mais" aparece quando subtypes > 5; subtype selecionado sempre visível.
- Detalhes: para cada categoria (
-
T020 [P] [US3] Aplicar
TruncatedFilterListna lista de bairros (visibleNeighborhoods) na seção "Localização" emfrontend/src/components/FilterSidebar.tsx- Detalhes: substituir o
visibleNeighborhoods.map(...)existente por<TruncatedFilterList items={visibleNeighborhoods} selectedId={filters.neighborhood_id ?? null} getId={n => n.id} getCount={n => n.property_count ?? 0} renderItem={(n, isPopular) => <...checkbox/chip existente...>{isPopular && <PopularBadge />}</...>} />; aplica somente quandovisibleNeighborhoods.length > 0. - Done: bairros ordenados por property_count DESC; top-5 visíveis; badge nos 3 primeiros; bairro selecionado sempre visível.
- Detalhes: substituir o
-
T021 [P] [US3] Aplicar
TruncatedFilterListnos grupos de comodidades na seção "Comodidades" emfrontend/src/components/FilterSidebar.tsx- Detalhes: para cada
AmenityGroup, substituir oamenities.filter(...).map(...)por<TruncatedFilterList>passandogetCount={a => a.property_count ?? 0}; comoAmenitynão temidcomo filterKey direto (usaamenity_idstoggle), usarselectedId={undefined}(amenidades não têm seleção única) — botão "Ver mais" ainda funciona para truncar a lista longa. - Done: cada grupo de comodidades exibe top-5; "Ver mais" expande; badge nos 3 mais populares por grupo; sem quebra no toggle de amenidades existente.
- Detalhes: para cada
Checkpoint US3: Abrir /imoveis → seção "Bairros" mostra top-5 com badge "Popular" no primeiro ✓. Selecionar bairro fora do top-5 → reabre seção → bairro visível mesmo sem "Ver mais" ✓. property_count: 0 para cidade sem imóveis ✓.
Phase Final: Polish & Validação
Purpose: Verificações de qualidade cross-cutting após todas as user stories implementadas.
-
T022 [P] Verificar integração end-to-end em
frontend/src/components/FilterSidebar.tsxe endpoints- Detalhes: com o backend rodando, abrir
/imoveis, inspecionar Network tab — confirmar queGET /api/v1/cities,/api/v1/neighborhoodse/api/v1/property-typesretornamproperty_countem todos os itens; confirmar no sidebar que a ordenação por popularidade está correta e os badges aparecem nos 3 itens com maior contagem em cada seção. - Done: nenhuma seção exibe itens sem
property_count; ordenação no sidebar reflete os valores do backend; badge "Popular" nos 3 corretos por categoria.
- Detalhes: com o backend rodando, abrir
-
T023 [P] Validar acessibilidade do
FilterSidebaremfrontend/src/components/FilterSidebar.tsx- Detalhes: verificar
aria-expandedcorreto nas seções (deve refletiropenSections[key]);SidebarSearchInputtemaria-label="Buscar filtro";SuggestionListitens têmrole="option"ou são<button>com label descritivo; navegação ↑↓ não produz erro de console;PopularBadgetemaria-label="Popular"ou éaria-hiddenconforme contexto. - Done: nenhum erro de acessibilidade no console;
aria-expandedcorreto em todas as seções; field de busca anunciado por screen reader;tsc --noEmitpassa.
- Detalhes: verificar
-
T024 Executar cenários do
quickstart.mdpara a feature 024 (quando disponível)- Done: todos os acceptance scenarios de US1, US2 e US3 do
spec.mdsão verificados manualmente ou via quickstart; nenhuma regressão em funcionalidades existentes do sidebar.
- Done: todos os acceptance scenarios de US1, US2 e US3 do
Dependencies & Execution Order
Phase Dependencies
Phase 1 (Backend) ──────────────────────────────────────────────┐
Phase 2 (Frontend Types) ─────────────────────────────────────── ┤→ Phase 5 (US3)
Phase 3 (US2 — Section Controlled) ──────────────────────────── ┤→ Phase 5 (US3)
Phase 3 (US2 — Section Controlled) ──────────────────────────── ┘→ Phase 4 (US1)
Phase 4 (US1 — Busca) → Phase Final
Phase 5 (US3 — Truncamento) → Phase Final
- Phase 1 e Phase 2: independentes entre si — podem rodar em paralelo
- Phase 3 (US2): independente de Phase 1/2 — pode iniciar imediatamente
- Phase 4 (US1): depende de T009 (US2) para
expandSection - Phase 5 (US3): depende de T001–T005 (property_count) + T008 (Section controlled)
- Phase Final: depende de todas as fases anteriores
User Story Dependencies
- US2 (P1): sem dependências — pode iniciar imediatamente
- US1 (P1): depende de US2 (T008, T009) para
expandSection; pode ser implementada em paralelo com Phase 1/2 - US3 (P2): depende de Phase 1 (T001–T004), Phase 2 (T005) e US2 (T008–T010)
Parallel Opportunities (dentro das fases)
- Phase 1: T003 e T004 marcadas [P] — edições em funções distintas do mesmo arquivo (
locations.py) - Phase 2: T005 [P] — arquivo distinto de toda a Phase 1
- Phase 5: T020 e T021 marcadas [P] — seções distintas do componente
Parallel Execution — MVP Scope
O MVP mínimo para entregar valor imediato é US2 + US1 (ambas P1, sem dados novos de backend):
Sequência MVP (US2 → US1):
T006 → T007 → T008 → T009 → T010 (US2: ~2h)
↓
T011 → T012 → T013 → T014 → T015 → T016 (US1: ~3h)
Para US3, adicionar antes:
Paralelo (pode rodar simultâneo ao MVP):
T001 → T002 (catalog.py)
T001 → T003 → T004 (locations.py)
T005 (catalog.ts)
Implementation Strategy
- Iniciar com US2 (T006–T010): menor risco, sem dados novos, entrega imediata de UX limpa
- Continuar com US1 (T011–T016): depende de US2; lógica mais complexa mas totalmente local
- Backend em paralelo (T001–T004): pode ser feito enquanto US1/US2 avançam no frontend
- Finalizar com US3 (T017–T021): após backend + US2; adiciona camada de popularidade
- Polish (T022–T024): validação final antes do merge
Summary
| Fase | Tarefas | User Story | Paralelo |
|---|---|---|---|
| Phase 1 — Backend | T001–T004 | (foundational US3) | T003, T004 [P] |
| Phase 2 — Types | T005 | (foundational US3) | T005 [P] |
| Phase 3 — US2 | T006–T010 | US2 | — |
| Phase 4 — US1 | T011–T016 | US1 | — |
| Phase 5 — US3 | T017–T021 | US3 | T020, T021 [P] |
| Phase Final | T022–T024 | — | T022, T023 [P] |
| Total | 24 tarefas | 3 user stories | 5 paralelas |