feat: features 025-032 - favoritos, contatos, trabalhe-conosco, area-cliente, navbar, hero-light-dark, performance-homepage
- feat(025): favoritos locais com FavoritesContext, HeartButton, PublicFavoritesPage
- feat(026): central de contatos admin (leads/contatos unificados)
- feat(027): configuração da página de contato via admin
- feat(028): trabalhe conosco - candidaturas com upload e admin
- feat(029): UX área do cliente - visitas, comparação, perfil
- feat(030): navbar UX - menu mobile, ThemeToggle, useFavorites
- feat(031): hero light/dark - imagens separadas por tema, upload, preview, seed
- feat(032): performance homepage - Promise.all parallel fetches, sessionStorage cache,
preload hero image, loading=lazy nos cards, useInView hook, will-change carrossel,
keyframes em index.css, AgentsCarousel e HomeScrollScene via props
- fix: light mode HomeScrollScene - gradiente, cores de texto, scroll hint
migrations: g1h2i3j4k5l6 (source em leads), h1i2j3k4l5m6 (contact_config),
i1j2k3l4m5n6 (job_applications), j2k3l4m5n6o7 (hero theme images)
This commit is contained in:
parent
6ef5a7a17e
commit
cf5603243c
106 changed files with 11927 additions and 1367 deletions
36
specs/029-ux-area-do-cliente/checklists/requirements.md
Normal file
36
specs/029-ux-area-do-cliente/checklists/requirements.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Specification Quality Checklist: Revisão UX/UI — Área do Cliente
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-04-22
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- Spec gerada a partir do UX audit `ux-audit.md` existente no mesmo diretório. Todos os itens aprovados na primeira validação.
|
||||
- Remoção de BoletosPage é apenas frontend; backend/model preservados — documentado em Assumptions.
|
||||
- Pronto para `/speckit.plan`.
|
||||
118
specs/029-ux-area-do-cliente/plan.md
Normal file
118
specs/029-ux-area-do-cliente/plan.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# Implementation Plan: Revisao UX/UI - Area do Cliente
|
||||
|
||||
**Branch**: `029-ux-area-do-cliente` | **Date**: 2026-04-22 | **Spec**: [spec.md](spec.md)
|
||||
**Input**: Feature specification from `specs/029-ux-area-do-cliente/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Esta feature simplifica a area do cliente removendo etapas de baixo valor (dashboard e boletos no frontend), melhora a navegacao contextual (menu com 4 itens e iconografia SVG consistente), aumenta a qualidade de leitura de favoritos (thumbnail, preco e localizacao), adiciona a acao de cancelamento de visitas pendentes e cria a pagina Minha Conta com atualizacao de nome e troca de senha.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: Python 3.12 (backend) + TypeScript 5.5 (frontend)
|
||||
**Primary Dependencies**: Flask 3.x, SQLAlchemy 2.x, Pydantic v2, bcrypt (backend); React 18, react-router-dom v6, Axios, Tailwind CSS 3.4 (frontend)
|
||||
**Storage**: PostgreSQL 16 (sem novas tabelas nesta feature)
|
||||
**Testing**: `pytest` para backend; `npm run build` para frontend; checklist manual de UX
|
||||
**Target Platform**: SPA web responsiva (desktop/mobile)
|
||||
**Project Type**: web app full-stack com foco principal em UX frontend
|
||||
**Performance Goals**: navegacao da area do cliente sem etapa intermediaria e atualizacoes sem reload para acoes principais
|
||||
**Constraints**: nao alterar schemas do banco; manter auth JWT existente; manter backend de boletos intacto
|
||||
**Scale/Scope**: rotas e componentes da area do cliente + 3 endpoints PATCH no backend
|
||||
|
||||
## Constitution Check
|
||||
|
||||
| Principio | Status | Observacao |
|
||||
|---|---|---|
|
||||
| Design-First | PASS | Mudancas focadas em fluxo, hierarquia visual e consistencia de icones |
|
||||
| Separation of Concerns | PASS | Regras de dominio no backend e comportamento de tela no frontend |
|
||||
| Spec-Driven | PASS | Plano derivado de `spec.md`, `ux-audit.md` e `tasks.md` |
|
||||
| Data Integrity | PASS | Sem migrations de schema; update em entidades existentes |
|
||||
| Security | PASS | Endpoints novos protegidos por JWT e ownership checks |
|
||||
| Simplicity First | PASS | Remove telas redundantes e reduz friccao de navegacao |
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/029-ux-area-do-cliente/
|
||||
├── plan.md
|
||||
├── spec.md
|
||||
├── tasks.md
|
||||
├── ux-audit.md
|
||||
└── checklists/
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
backend/
|
||||
└── app/
|
||||
├── routes/client_area.py
|
||||
└── schemas/client_area.py
|
||||
|
||||
frontend/
|
||||
└── src/
|
||||
├── App.tsx
|
||||
├── layouts/ClientLayout.tsx
|
||||
├── contexts/AuthContext.tsx
|
||||
├── services/clientArea.ts
|
||||
├── types/clientArea.ts
|
||||
└── pages/client/
|
||||
├── FavoritesPage.tsx
|
||||
├── VisitsPage.tsx
|
||||
├── ComparisonPage.tsx
|
||||
└── ProfilePage.tsx
|
||||
```
|
||||
|
||||
**Structure Decision**: manter estrutura web existente, com mudancas localizadas em rotas/schemas backend e paginas/layouts da area do cliente no frontend.
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
### 1. Roteamento e navegacao
|
||||
|
||||
- Redirecionar index da area do cliente para favoritos.
|
||||
- Remover rota e item visual de boletos no frontend.
|
||||
- Introduzir rota Minha Conta e manter destaque de item ativo em desktop/mobile.
|
||||
|
||||
### 2. Favoritos com contexto visual
|
||||
|
||||
- Enriquecer payload de favoritos no backend com dados de card (`price`, `city`, `neighborhood`, `cover_photo_url`).
|
||||
- Atualizar card de favoritos para exibicao direta desses dados com fallback de imagem.
|
||||
- Manter remocao sem reload com comportamento otimista.
|
||||
|
||||
### 3. Visitas com acao de cancelamento
|
||||
|
||||
- Exibir botao de cancelamento apenas para `status=pending`.
|
||||
- Criar endpoint de cancelamento com validacao de ownership e estado atual.
|
||||
- Aplicar update otimista no frontend com rollback em erro.
|
||||
|
||||
### 4. Minha Conta
|
||||
|
||||
- Criar tela `/area-do-cliente/conta` com formulario de nome e formulario de senha.
|
||||
- Criar endpoints `PATCH /me/profile` e `PATCH /me/password`.
|
||||
- Expor `updateUser` no `AuthContext` para refletir nome atualizado imediatamente.
|
||||
|
||||
### 5. Qualidade visual e consistencia
|
||||
|
||||
- Trocar iconografia por SVG Heroicons no layout do cliente.
|
||||
- Melhorar empty states e feedbacks de erro/sucesso nas principais interacoes.
|
||||
|
||||
## Rollout and Validation
|
||||
|
||||
### Executavel
|
||||
|
||||
- Frontend: `npm run build`
|
||||
- Backend: `pytest` nos cenarios relevantes de cliente (quando disponiveis)
|
||||
|
||||
### Manual
|
||||
|
||||
- Verificar redirecionamento `/area-do-cliente -> /area-do-cliente/favoritos`
|
||||
- Verificar ausencia de menu/rota de boletos no frontend
|
||||
- Verificar card de favorito com imagem/preco/localizacao
|
||||
- Verificar cancelamento de visita pendente e bloqueio para estados nao pendentes
|
||||
- Verificar atualizacao de nome e troca de senha em Minha Conta
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
Nenhuma excecao arquitetural requerida para esta feature.
|
||||
203
specs/029-ux-area-do-cliente/spec.md
Normal file
203
specs/029-ux-area-do-cliente/spec.md
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# Feature Specification: Revisão UX/UI — Área do Cliente
|
||||
|
||||
**Feature Branch**: `029-ux-area-do-cliente`
|
||||
**Created**: 2026-04-22
|
||||
**Status**: Draft
|
||||
|
||||
---
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 — Navegar direto para Favoritos ao acessar a Área do Cliente (Priority: P1)
|
||||
|
||||
O cliente autenticado digita ou clica no link da Área do Cliente e chega imediatamente à lista de imóveis favoritos, sem passar por uma tela de painel intermediária que não agrega valor.
|
||||
|
||||
**Why this priority**: Elimina um clique desnecessário presente hoje. É a mudança de maior impacto imediato na jornada do usuário e desbloqueio para todo o restante da área.
|
||||
|
||||
**Independent Test**: Acessar `/area-do-cliente` deve redirecionar para `/area-do-cliente/favoritos`. A página de Favoritos deve carregar de forma autônoma e ser completamente utilizável.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** um cliente autenticado, **When** ele acessa `/area-do-cliente`, **Then** é redirecionado automaticamente para `/area-do-cliente/favoritos`.
|
||||
2. **Given** um cliente autenticado, **When** ele acessa `/area-do-cliente/boletos`, **Then** recebe uma resposta 404 (rota inexistente).
|
||||
3. **Given** um cliente autenticado, **When** ele visualiza o menu lateral, **Then** os itens "Painel" e "Boletos" não estão presentes.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 — Visualizar favoritos com imagem, preço e localização (Priority: P1)
|
||||
|
||||
O cliente vê seus imóveis favoritos exibidos em cards que mostram a thumbnail da propriedade, o preço e a cidade/bairro, permitindo identificar cada imóvel sem precisar clicar para abrir.
|
||||
|
||||
**Why this priority**: É a página mais acessada da área do cliente. Sem imagem e preço, o usuário não consegue distinguir os imóveis e a experiência é frustrante.
|
||||
|
||||
**Independent Test**: Com ao menos um favorito salvo, a página deve exibir cards com imagem (ou placeholder), preço formatado e localização.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** um cliente com favoritos salvos, **When** ele acessa `/area-do-cliente/favoritos`, **Then** cada card exibe thumbnail da propriedade (ou placeholder se sem foto), título, preço e cidade/bairro.
|
||||
2. **Given** um cliente sem favoritos, **When** ele acessa a página, **Then** vê um empty state informativo com link para `/imoveis`.
|
||||
3. **Given** um cliente visualizando favoritos, **When** ele clica em "Ver imóvel", **Then** é levado à página de detalhes do imóvel.
|
||||
4. **Given** um cliente visualizando favoritos, **When** ele remove um favorito, **Then** o card some da lista sem recarregar a página.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 — Cancelar visita agendada (Priority: P2)
|
||||
|
||||
O cliente pode solicitar o cancelamento de uma visita com status pendente diretamente pela Área do Cliente, sem precisar entrar em contato por outro canal.
|
||||
|
||||
**Why this priority**: Resolve um ponto de atrito real: hoje o usuário não tem como cancelar uma visita pelo sistema, o que gera contato desnecessário via WhatsApp ou telefone.
|
||||
|
||||
**Independent Test**: Com uma visita no status "pendente", o botão "Cancelar" deve ser exibido e, ao clicar, alterar o status para "cancelado".
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** uma visita com `status=pending`, **When** o cliente acessa `/area-do-cliente/visitas`, **Then** o card exibe o botão "Cancelar".
|
||||
2. **Given** o cliente clica em "Cancelar", **When** a ação é confirmada, **Then** o status do card muda para "cancelado" e o botão desaparece.
|
||||
3. **Given** uma visita com status diferente de `pending` (ex: confirmada, cancelada), **When** o cliente visualiza o card, **Then** o botão "Cancelar" não está disponível.
|
||||
4. **Given** uma falha de rede ao cancelar, **When** a requisição retorna erro, **Then** o cliente vê uma mensagem de erro e o status não muda.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 — Editar perfil e trocar senha (Priority: P2)
|
||||
|
||||
O cliente pode acessar `/area-do-cliente/conta`, visualizar seus dados cadastrais, atualizar o nome e alterar a senha, tudo dentro da Área do Cliente.
|
||||
|
||||
**Why this priority**: Hoje não existe nenhuma forma do cliente atualizar seus dados pelo sistema. É uma funcionalidade básica esperada em qualquer área logada.
|
||||
|
||||
**Independent Test**: A página `/area-do-cliente/conta` deve existir com formulário funcional de atualização de nome e de troca de senha.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** o cliente acessa `/area-do-cliente/conta`, **When** a página carrega, **Then** exibe o nome atual preenchido e o e-mail em campo somente leitura.
|
||||
2. **Given** o cliente altera o nome e clica em "Salvar", **When** o nome é válido (não vazio), **Then** vê confirmação de sucesso e o nome atualizado no menu lateral.
|
||||
3. **Given** o cliente preenche "Senha atual", "Nova senha" e "Confirmar nova senha", **When** as senhas coincidem e têm pelo menos 8 caracteres, **Then** a senha é alterada e uma mensagem de sucesso é exibida.
|
||||
4. **Given** o cliente informa uma senha atual incorreta, **When** submete o form, **Then** vê mensagem de erro "Senha atual incorreta" sem alterar nada.
|
||||
5. **Given** "Nova senha" e "Confirmar nova senha" divergem, **When** o cliente tenta salvar, **Then** vê validação inline "As senhas não coincidem" antes de enviar ao servidor.
|
||||
|
||||
---
|
||||
|
||||
### User Story 5 — Navegação com ícones consistentes e item "Minha conta" (Priority: P3)
|
||||
|
||||
O cliente vê o menu lateral (desktop) e a barra de navegação (mobile) com ícones vetoriais consistentes em qualquer sistema operacional, e um link "Minha conta" que leva à nova página de perfil.
|
||||
|
||||
**Why this priority**: Melhoria visual e de consistência; não bloqueia funcionalidades principais, mas impacta a percepção de qualidade do produto.
|
||||
|
||||
**Independent Test**: O menu deve renderizar 4 itens (Favoritos, Comparar, Visitas, Minha conta) com ícones SVG visíveis e corretos em temas claro e escuro.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** o cliente está na Área do Cliente, **When** visualiza o menu lateral ou a barra mobile, **Then** os 4 itens (Favoritos, Comparar, Visitas, Minha conta) estão presentes com ícones SVG.
|
||||
2. **Given** o cliente está em qualquer página da área do cliente, **When** observa o item de menu correspondente à página atual, **Then** o item está visualmente destacado (ativo).
|
||||
3. **Given** o cliente está em mobile, **When** acessa a barra de navegação, **Then** os 4 itens estão centralizados sem scroll horizontal desnecessário.
|
||||
4. **Given** o cliente clica em "Sair", **When** a ação é executada, **Then** o ícone do botão é o ícone de logout (seta saindo de uma porta), não uma seta genérica.
|
||||
|
||||
---
|
||||
|
||||
### User Story 6 — Empty state explicativo no Comparar (Priority: P3)
|
||||
|
||||
O cliente que acessa a página de comparação sem imóveis selecionados recebe uma instrução clara sobre como adicionar imóveis à comparação.
|
||||
|
||||
**Why this priority**: Melhoria de usabilidade pontual; sem ela o usuário fica confuso, mas o impacto é menor que os itens anteriores.
|
||||
|
||||
**Independent Test**: Acessar `/area-do-cliente/comparar` sem imóveis selecionados deve exibir o empty state com a instrução de uso.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** o cliente não tem imóveis na comparação, **When** acessa `/area-do-cliente/comparar`, **Then** vê uma mensagem explicando como adicionar imóveis (ex: "Acesse um imóvel e clique em 'Comparar' para adicioná-lo aqui").
|
||||
2. **Given** o cliente tem imóveis na comparação, **When** acessa a página, **Then** a tabela de comparação é exibida normalmente.
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- O que acontece se a foto de um imóvel favorito for removida após ser salvo? → O card exibe um placeholder de imagem.
|
||||
- O que acontece se o cliente tentar cancelar uma visita já cancelada por outra aba? → O servidor retorna erro e o frontend exibe mensagem informativa.
|
||||
- O que acontece se o cliente enviar o form de perfil com nome vazio? → Validação inline impede o envio antes de chegar ao servidor.
|
||||
- O que acontece se o cliente sem favoritos tentar acessar diretamente `/area-do-cliente`? → Redireciona para `/area-do-cliente/favoritos` e exibe o empty state de favoritos.
|
||||
- O que acontece se a sessão expirar durante uso da área do cliente? → O cliente é redirecionado para a tela de login com mensagem de sessão expirada.
|
||||
|
||||
---
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
**Roteamento e Estrutura**
|
||||
|
||||
- **FR-001**: O sistema DEVE redirecionar `/area-do-cliente` para `/area-do-cliente/favoritos` de forma automática.
|
||||
- **FR-002**: A rota `/area-do-cliente/boletos` DEVE ser removida; acessá-la DEVE retornar 404.
|
||||
- **FR-003**: O menu de navegação da Área do Cliente DEVE conter exatamente 4 itens: Favoritos, Comparar, Visitas e Minha conta.
|
||||
- **FR-004**: Os ícones do menu DEVE ser SVG vetoriais (Heroicons 2.0 outline), sem uso de emoji ou caracteres Unicode.
|
||||
|
||||
**Favoritos**
|
||||
|
||||
- **FR-005**: Cada card de favorito DEVE exibir: thumbnail da propriedade (ou placeholder padronizado), título, preço formatado em reais e cidade/bairro.
|
||||
- **FR-006**: A thumbnail DEVE ser obtida a partir dos dados da propriedade (campo `cover_photo` ou primeira foto da galeria).
|
||||
- **FR-007**: O card DEVE oferecer as ações "Ver imóvel" (navega para detalhe) e "Remover dos favoritos" (remove sem recarregar a página).
|
||||
|
||||
**Visitas**
|
||||
|
||||
- **FR-008**: O card de visita DEVE exibir: título do imóvel em destaque, data agendada/solicitada em destaque, badge de status alinhado à direita e mensagem como texto secundário.
|
||||
- **FR-009**: Visitas com `status=pending` DEVE exibir botão "Cancelar".
|
||||
- **FR-010**: Ao confirmar o cancelamento, o sistema DEVE chamar `PATCH /me/visits/:id/cancel` e atualizar o status do card para "cancelado" sem recarregar a página.
|
||||
- **FR-011**: Visitas com status diferente de `pending` NÃO DEVEM exibir o botão "Cancelar".
|
||||
|
||||
**Comparar**
|
||||
|
||||
- **FR-012**: Quando não há imóveis selecionados para comparação, a página DEVE exibir um empty state com instrução clara de como adicionar imóveis à comparação.
|
||||
|
||||
**Perfil / Minha conta**
|
||||
|
||||
- **FR-013**: A rota `/area-do-cliente/conta` DEVE ser criada e acessível pelo menu "Minha conta".
|
||||
- **FR-014**: A página de conta DEVE exibir o nome atual do cliente em campo editável e o e-mail em campo somente leitura.
|
||||
- **FR-015**: O cliente DEVE poder atualizar o nome via `PATCH /me/profile`; o campo nome é obrigatório.
|
||||
- **FR-016**: O cliente DEVE poder trocar a senha informando senha atual, nova senha e confirmação via `PATCH /me/password`.
|
||||
- **FR-017**: A nova senha DEVE ter no mínimo 8 caracteres; a confirmação DEVE ser idêntica à nova senha; validações DEVEM ocorrer no frontend antes do envio.
|
||||
- **FR-018**: Se a senha atual informada estiver incorreta, o servidor DEVE retornar erro e o frontend DEVE exibir "Senha atual incorreta".
|
||||
|
||||
**Backend — Novos Endpoints**
|
||||
|
||||
- **FR-019**: `PATCH /me/profile` — atualiza o nome do cliente autenticado. Requer autenticação JWT. Retorna os dados atualizados.
|
||||
- **FR-020**: `PATCH /me/password` — altera a senha do cliente autenticado. Valida senha atual antes de persistir. Requer autenticação JWT.
|
||||
- **FR-021**: `PATCH /me/visits/:id/cancel` — cancela uma visita com `status=pending` pertencente ao cliente autenticado. Retorna a visita atualizada. Requer autenticação JWT.
|
||||
- **FR-022**: Tentativa de cancelar visita com status diferente de `pending` DEVE retornar erro com mensagem descritiva.
|
||||
- **FR-023**: Tentativa de cancelar visita de outro cliente DEVE retornar 403 Forbidden.
|
||||
|
||||
**Layout e Mobile**
|
||||
|
||||
- **FR-024**: O botão "Sair" DEVE usar o ícone ArrowRightOnRectangle (Heroicons) no lugar do caractere `→`.
|
||||
- **FR-025**: Em viewport mobile, a barra de navegação DEVE centralizar os 4 itens e indicar visualmente o item ativo.
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **ClientUser**: Usuário da área do cliente. Atributos relevantes: `id`, `name`, `email`, `password_hash`. Possui coleções de favoritos e visitas.
|
||||
- **SavedProperty (Favorito)**: Associação entre `ClientUser` e `Property`. Atributos relevantes: `id`, `client_user_id`, `property_id`, `created_at`.
|
||||
- **VisitRequest (Visita)**: Solicitação de visita feita por um `ClientUser` para uma `Property`. Atributos relevantes: `id`, `client_user_id`, `property_id`, `scheduled_date`, `status` (pending | confirmed | cancelled), `message`.
|
||||
- **Property (Imóvel)**: Imóvel do catálogo. Atributos consumidos nesta feature: `id`, `title`, `price`, `city`, `neighborhood`, `cover_photo` (ou galeria de fotos).
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: O cliente chega ao conteúdo real (Favoritos) com um clique a menos do que hoje — a página de painel não existe mais como etapa intermediária.
|
||||
- **SC-002**: O cliente consegue identificar visualmente seus imóveis favoritos sem precisar abrir nenhum deles — cada card mostra imagem, preço e localização.
|
||||
- **SC-003**: O cliente consegue cancelar uma visita pendente em menos de 3 cliques a partir da página de Visitas, sem sair da Área do Cliente.
|
||||
- **SC-004**: O cliente consegue atualizar nome ou senha em uma única interação com o formulário de conta, sem precisar entrar em contato com suporte.
|
||||
- **SC-005**: Os ícones do menu são renderizados de forma idêntica em Windows, macOS e Linux, tanto em tema claro quanto escuro.
|
||||
- **SC-006**: A taxa de abandono da Área do Cliente (saída imediata) deve ser reduzida, dado que o primeiro conteúdo exibido passa a ser imediatamente útil.
|
||||
- **SC-007**: Todos os 3 novos endpoints do backend respondem corretamente com autenticação válida e retornam erros descritivos para entradas inválidas.
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
- A tabela `client_users` já existe no banco com as colunas `id`, `name`, `email` e `password_hash`; nenhuma migration de schema é necessária para os endpoints de perfil e senha.
|
||||
- A tabela `visit_requests` já possui a coluna `status` com os valores `pending`, `confirmed` e `cancelled`; o endpoint de cancelamento apenas atualiza este campo.
|
||||
- O backend de favoritos já expõe o `property_id`; a foto do imóvel será obtida do campo `cover_photo` da tabela de propriedades ou do primeiro item retornado pela API de fotos já existente.
|
||||
- O sistema de autenticação JWT para clientes já está operacional; os novos endpoints reutilizarão o mesmo middleware de autenticação.
|
||||
- A remoção de `BoletosPage` é apenas frontend e de rota; o model e os dados de boletos no banco são mantidos intactos para uso futuro pelo admin.
|
||||
- O componente de comparação já armazena os imóveis selecionados em estado local ou contexto; esta feature não altera o mecanismo de seleção, apenas o empty state.
|
||||
- Não há requisito de confirmação por e-mail para troca de senha nesta fase (fluxo simplificado: validar senha atual → salvar nova senha).
|
||||
- A feature não inclui upload de foto de perfil do cliente.
|
||||
994
specs/029-ux-area-do-cliente/tasks.md
Normal file
994
specs/029-ux-area-do-cliente/tasks.md
Normal file
|
|
@ -0,0 +1,994 @@
|
|||
# Tasks — Feature 029: Revisão UX/UI da Área do Cliente
|
||||
|
||||
**Branch**: `029-ux-area-do-cliente`
|
||||
**Gerado em**: 2026-04-22
|
||||
**Status**: Ready for implementation
|
||||
|
||||
---
|
||||
|
||||
## Fase 1 — Backend: Schemas
|
||||
|
||||
### T01 — Adicionar `PropertyCard` e schemas de profile/senha em `client_area.py`
|
||||
|
||||
**Arquivo**: `backend/app/schemas/client_area.py`
|
||||
|
||||
**Contexto**: O schema `PropertyBrief` só carrega `id`, `title`, `slug`. Os cards de favoritos precisam de `price`, `city`, `neighborhood` e `cover_photo_url`. Além disso, os endpoints de perfil e senha precisam de schemas de entrada/saída que não existem.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Abrir `backend/app/schemas/client_area.py`.
|
||||
2. Adicionar import `from typing import Optional` (já existe) e `from decimal import Decimal` (já existe).
|
||||
3. Adicionar o schema `PropertyCard` logo após `PropertyBrief`:
|
||||
```python
|
||||
class PropertyCard(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
slug: str
|
||||
price: Optional[Decimal] = None
|
||||
city: Optional[str] = None
|
||||
neighborhood: Optional[str] = None
|
||||
cover_photo_url: Optional[str] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@classmethod
|
||||
def from_property(cls, prop) -> "PropertyCard":
|
||||
"""Constrói PropertyCard a partir de um ORM Property."""
|
||||
cover = prop.photos[0].url if prop.photos else None
|
||||
city = prop.city.name if prop.city else None
|
||||
neighborhood = prop.neighborhood.name if prop.neighborhood else None
|
||||
return cls(
|
||||
id=str(prop.id),
|
||||
title=prop.title,
|
||||
slug=prop.slug,
|
||||
price=prop.price,
|
||||
city=city,
|
||||
neighborhood=neighborhood,
|
||||
cover_photo_url=cover,
|
||||
)
|
||||
```
|
||||
4. Alterar `SavedPropertyOut` para usar `PropertyCard` em vez de `PropertyBrief`:
|
||||
```python
|
||||
class SavedPropertyOut(BaseModel):
|
||||
id: str
|
||||
property_id: Optional[str]
|
||||
property: Optional[PropertyCard] # era PropertyBrief
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
```
|
||||
5. Adicionar os schemas de profile e senha ao final do arquivo:
|
||||
```python
|
||||
class UpdateProfileIn(BaseModel):
|
||||
name: str
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def name_not_empty(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("Nome não pode ser vazio")
|
||||
return v.strip()
|
||||
|
||||
|
||||
class UpdateProfileOut(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
email: str
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class UpdatePasswordIn(BaseModel):
|
||||
current_password: str
|
||||
new_password: str
|
||||
|
||||
@field_validator("new_password")
|
||||
@classmethod
|
||||
def min_length(cls, v: str) -> str:
|
||||
if len(v) < 8:
|
||||
raise ValueError("A nova senha deve ter pelo menos 8 caracteres")
|
||||
return v
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] `PropertyCard` existe em `client_area.py` com os campos: `id`, `title`, `slug`, `price`, `city`, `neighborhood`, `cover_photo_url`.
|
||||
- [ ] `PropertyCard.from_property()` extrai `photos[0].url` como cover e `city.name` / `neighborhood.name` dos relacionamentos ORM.
|
||||
- [ ] `SavedPropertyOut.property` usa `PropertyCard` (não mais `PropertyBrief`).
|
||||
- [ ] `UpdateProfileIn`, `UpdateProfileOut` e `UpdatePasswordIn` existem com as validações descritas.
|
||||
|
||||
---
|
||||
|
||||
## Fase 2 — Backend: Endpoints
|
||||
|
||||
### T02 — Adicionar eager load de fotos na query de favoritos
|
||||
|
||||
**Arquivo**: `backend/app/routes/client_area.py`
|
||||
|
||||
**Contexto**: `SavedProperty.property` usa `lazy="joined"`, mas `Property.photos` usa `lazy="select"`. Sem `selectinload`, cada card de favorito dispara uma query extra para buscar as fotos (N+1). Precisa carregar as fotos junto com os favoritos.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Adicionar import no topo do arquivo:
|
||||
```python
|
||||
from sqlalchemy.orm import selectinload
|
||||
from app.models.property import Property as PropertyModel
|
||||
```
|
||||
2. Localizar a função `get_favorites` (rota `GET /favorites`).
|
||||
3. Substituir a query atual:
|
||||
```python
|
||||
# Antes:
|
||||
saved = (
|
||||
SavedProperty.query.filter_by(user_id=g.current_user_id)
|
||||
.order_by(SavedProperty.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
```
|
||||
Por:
|
||||
```python
|
||||
# Depois:
|
||||
saved = (
|
||||
SavedProperty.query
|
||||
.filter_by(user_id=g.current_user_id)
|
||||
.options(selectinload(SavedProperty.property).selectinload(PropertyModel.photos))
|
||||
.order_by(SavedProperty.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
```
|
||||
4. Atualizar os imports de schemas no topo do arquivo — `SavedPropertyOut` agora serializa `PropertyCard`; não é necessária mudança de código aqui pois o schema foi alterado em T01, mas verificar que `PropertyCard` está disponível via `SavedPropertyOut`.
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] A query de favoritos usa `selectinload` para `property` → `photos`.
|
||||
- [ ] O endpoint `GET /me/favorites` retorna o campo `property.cover_photo_url` com a URL da primeira foto (ou `null`).
|
||||
- [ ] O endpoint retorna `property.price`, `property.city`, `property.neighborhood`.
|
||||
- [ ] Testado manualmente: chamada para `GET /api/me/favorites` retorna JSON com campos `price`, `city`, `neighborhood`, `cover_photo_url` no objeto `property`.
|
||||
|
||||
---
|
||||
|
||||
### T03 — Implementar `PATCH /me/profile`
|
||||
|
||||
**Arquivo**: `backend/app/routes/client_area.py`
|
||||
|
||||
**Contexto**: Endpoint inexistente. Deve atualizar `ClientUser.name` do usuário autenticado.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Adicionar import:
|
||||
```python
|
||||
import bcrypt
|
||||
from app.models.user import ClientUser
|
||||
from app.schemas.client_area import UpdateProfileIn, UpdateProfileOut, UpdatePasswordIn
|
||||
```
|
||||
2. Adicionar ao final do arquivo (antes de qualquer `if __name__`):
|
||||
```python
|
||||
@client_bp.patch("/profile")
|
||||
@require_auth
|
||||
def update_profile():
|
||||
try:
|
||||
data = UpdateProfileIn.model_validate(request.get_json() or {})
|
||||
except ValidationError as e:
|
||||
return jsonify({"error": e.errors(include_url=False)}), 422
|
||||
|
||||
user = db.session.get(ClientUser, g.current_user_id)
|
||||
if not user:
|
||||
return jsonify({"error": "Usuário não encontrado"}), 404
|
||||
|
||||
user.name = data.name
|
||||
db.session.commit()
|
||||
return jsonify(UpdateProfileOut.model_validate(user).model_dump(mode="json")), 200
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] `PATCH /api/me/profile` com `{ "name": "Novo Nome" }` retorna `200` com `{ id, name, email }`.
|
||||
- [ ] `PATCH /api/me/profile` com `{ "name": "" }` retorna `422`.
|
||||
- [ ] `PATCH /api/me/profile` sem token retorna `401`.
|
||||
|
||||
---
|
||||
|
||||
### T04 — Implementar `PATCH /me/password`
|
||||
|
||||
**Arquivo**: `backend/app/routes/client_area.py`
|
||||
|
||||
**Contexto**: Endpoint inexistente. Deve verificar senha atual com bcrypt antes de gravar a nova.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Garantir que `bcrypt` e `ClientUser` estão importados (feito em T03).
|
||||
2. Adicionar ao final do arquivo:
|
||||
```python
|
||||
@client_bp.patch("/password")
|
||||
@require_auth
|
||||
def change_password():
|
||||
try:
|
||||
data = UpdatePasswordIn.model_validate(request.get_json() or {})
|
||||
except ValidationError as e:
|
||||
return jsonify({"error": e.errors(include_url=False)}), 422
|
||||
|
||||
user = db.session.get(ClientUser, g.current_user_id)
|
||||
if not user:
|
||||
return jsonify({"error": "Usuário não encontrado"}), 404
|
||||
|
||||
# Verifica senha atual
|
||||
if not bcrypt.checkpw(
|
||||
data.current_password.encode("utf-8"),
|
||||
user.password_hash.encode("utf-8"),
|
||||
):
|
||||
return jsonify({"error": "Senha atual incorreta"}), 400
|
||||
|
||||
user.password_hash = bcrypt.hashpw(
|
||||
data.new_password.encode("utf-8"), bcrypt.gensalt()
|
||||
).decode("utf-8")
|
||||
db.session.commit()
|
||||
return "", 204
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] `PATCH /api/me/password` com senha atual correta e nova senha ≥ 8 chars retorna `204`.
|
||||
- [ ] `PATCH /api/me/password` com senha atual incorreta retorna `400` com `"Senha atual incorreta"`.
|
||||
- [ ] `PATCH /api/me/password` com nova senha < 8 chars retorna `422`.
|
||||
- [ ] `PATCH /api/me/password` sem token retorna `401`.
|
||||
|
||||
---
|
||||
|
||||
### T05 — Implementar `PATCH /me/visits/<id>/cancel`
|
||||
|
||||
**Arquivo**: `backend/app/routes/client_area.py`
|
||||
|
||||
**Contexto**: Endpoint inexistente. Deve cancelar visita com `status=pending` do usuário autenticado.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Adicionar ao final do arquivo:
|
||||
```python
|
||||
@client_bp.patch("/visits/<visit_id>/cancel")
|
||||
@require_auth
|
||||
def cancel_visit(visit_id: str):
|
||||
visit = db.session.get(VisitRequest, visit_id)
|
||||
if not visit:
|
||||
return jsonify({"error": "Visita não encontrada"}), 404
|
||||
|
||||
if visit.user_id != g.current_user_id:
|
||||
return jsonify({"error": "Acesso negado"}), 403
|
||||
|
||||
if visit.status != "pending":
|
||||
return jsonify({"error": "Apenas visitas pendentes podem ser canceladas"}), 400
|
||||
|
||||
visit.status = "cancelled"
|
||||
db.session.commit()
|
||||
return jsonify(VisitRequestOut.model_validate(visit).model_dump(mode="json")), 200
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] `PATCH /api/me/visits/<id>/cancel` com visita `status=pending` do próprio usuário retorna `200` com `status: "cancelled"`.
|
||||
- [ ] Cancelar visita com `status=confirmed` retorna `400`.
|
||||
- [ ] Cancelar visita de outro usuário retorna `403`.
|
||||
- [ ] Cancelar visita inexistente retorna `404`.
|
||||
- [ ] `VisitRequestOut` está importado (já estava) e serializa o resultado corretamente.
|
||||
|
||||
---
|
||||
|
||||
## Fase 3 — Frontend: Tipos e Serviços
|
||||
|
||||
### T06 — Atualizar tipos em `clientArea.ts`
|
||||
|
||||
**Arquivo**: `frontend/src/types/clientArea.ts`
|
||||
|
||||
**Contexto**: `SavedProperty.property` só tem `{ id, title, slug }`. Precisa incluir `price`, `city`, `neighborhood`, `cover_photo_url`. Adicionar também os tipos dos payloads de profile/senha.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Substituir o tipo `SavedProperty`:
|
||||
```typescript
|
||||
export interface PropertyCard {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
price: string | null; // Decimal serializado como string pelo backend
|
||||
city: string | null;
|
||||
neighborhood: string | null;
|
||||
cover_photo_url: string | null;
|
||||
}
|
||||
|
||||
export interface SavedProperty {
|
||||
id: string;
|
||||
property_id: string | null;
|
||||
property: PropertyCard | null;
|
||||
created_at: string;
|
||||
}
|
||||
```
|
||||
2. Adicionar ao final do arquivo:
|
||||
```typescript
|
||||
export interface UpdateProfilePayload {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UpdateProfileResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface ChangePasswordPayload {
|
||||
current_password: string;
|
||||
new_password: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] `SavedProperty.property` é `PropertyCard | null`.
|
||||
- [ ] `PropertyCard` tem todos os 7 campos listados.
|
||||
- [ ] `UpdateProfilePayload`, `UpdateProfileResponse` e `ChangePasswordPayload` estão exportados.
|
||||
- [ ] Sem erros de TypeScript em arquivos que importam `SavedProperty`.
|
||||
|
||||
---
|
||||
|
||||
### T07 — Adicionar `updateProfile`, `changePassword` e `cancelVisit` em `clientArea.ts`
|
||||
|
||||
**Arquivo**: `frontend/src/services/clientArea.ts`
|
||||
|
||||
**Contexto**: O serviço atual tem `getFavorites`, `addFavorite`, `removeFavorite`, `getVisits`, `getBoletos`. Faltam os três novos métodos correspondentes aos endpoints criados em T03–T05.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Atualizar o import de tipos no topo:
|
||||
```typescript
|
||||
import type {
|
||||
Boleto,
|
||||
ChangePasswordPayload,
|
||||
SavedProperty,
|
||||
UpdateProfilePayload,
|
||||
UpdateProfileResponse,
|
||||
VisitRequest,
|
||||
} from '../types/clientArea';
|
||||
```
|
||||
2. Adicionar ao final do arquivo:
|
||||
```typescript
|
||||
export async function updateProfile(
|
||||
data: UpdateProfilePayload,
|
||||
): Promise<UpdateProfileResponse> {
|
||||
const response = await api.patch<UpdateProfileResponse>('/me/profile', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function changePassword(data: ChangePasswordPayload): Promise<void> {
|
||||
await api.patch('/me/password', data);
|
||||
}
|
||||
|
||||
export async function cancelVisit(visitId: string): Promise<VisitRequest> {
|
||||
const response = await api.patch<VisitRequest>(`/me/visits/${visitId}/cancel`);
|
||||
return response.data;
|
||||
}
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] `updateProfile`, `changePassword` e `cancelVisit` são exportados de `clientArea.ts`.
|
||||
- [ ] Tipos corretos: `updateProfile` retorna `Promise<UpdateProfileResponse>`, `cancelVisit` retorna `Promise<VisitRequest>`.
|
||||
- [ ] Sem erros TypeScript no arquivo.
|
||||
|
||||
---
|
||||
|
||||
## Fase 4 — Frontend: AuthContext
|
||||
|
||||
### T08 — Expor `updateUser` no `AuthContext`
|
||||
|
||||
**Arquivo**: `frontend/src/contexts/AuthContext.tsx`
|
||||
|
||||
**Contexto**: `ProfilePage` (T10) precisará atualizar `user.name` no contexto após salvar com sucesso, para que o sidebar reflita imediatamente o novo nome sem reload. Atualmente `setUser` é interno ao Provider.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Adicionar `updateUser` à interface `AuthContextValue`:
|
||||
```typescript
|
||||
interface AuthContextValue {
|
||||
user: User | null
|
||||
token: string | null
|
||||
isAuthenticated: boolean
|
||||
isLoading: boolean
|
||||
login: (data: LoginCredentials) => Promise<void>
|
||||
register: (data: RegisterCredentials) => Promise<void>
|
||||
logout: () => void
|
||||
updateUser: (partial: Partial<User>) => void // NOVO
|
||||
}
|
||||
```
|
||||
2. Implementar `updateUser` dentro do `AuthProvider`, após a declaração de `logout`:
|
||||
```typescript
|
||||
const updateUser = useCallback((partial: Partial<User>) => {
|
||||
setUser(prev => (prev ? { ...prev, ...partial } : prev))
|
||||
}, [])
|
||||
```
|
||||
3. Adicionar `updateUser` ao objeto passado para `AuthContext.Provider`:
|
||||
```typescript
|
||||
value={{
|
||||
user,
|
||||
token,
|
||||
isAuthenticated: !!user,
|
||||
isLoading,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
updateUser, // NOVO
|
||||
}}
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] `useAuth().updateUser({ name: "Novo Nome" })` atualiza `user.name` no contexto.
|
||||
- [ ] O sidebar (`ClientLayout.tsx`) reflete o novo nome sem recarga de página ao chamar `updateUser`.
|
||||
- [ ] Sem erros TypeScript no arquivo ou em consumidores de `useAuth`.
|
||||
|
||||
---
|
||||
|
||||
## Fase 5 — Frontend: Layout
|
||||
|
||||
### T09 — Refatorar `ClientLayout.tsx` com ícones SVG e nova navegação
|
||||
|
||||
**Arquivo**: `frontend/src/layouts/ClientLayout.tsx`
|
||||
|
||||
**Contexto**: O layout atual tem 5 itens de nav com emoji Unicode inconsistentes, inclui "Painel" e "Boletos", e o botão "Sair" usa `→`. A nova nav tem 4 itens com SVG Heroicons.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Substituir o array `navItems` por componentes SVG inline para cada ícone. Criar 4 componentes SVG pequenos no topo do arquivo (ou inline no array), usando Heroicons 2.0 outline (24×24, `stroke="currentColor"`, `strokeWidth={1.5}`):
|
||||
|
||||
- **Favoritos** → `HeartIcon` (coração vazio)
|
||||
- **Comparar** → `ScaleIcon` (balança/scale)
|
||||
- **Visitas** → `CalendarIcon` (calendário)
|
||||
- **Minha conta** → `UserCircleIcon` (usuário com círculo)
|
||||
- **Logout** → `ArrowRightOnRectangleIcon` (seta saindo de retângulo)
|
||||
|
||||
Exemplo de SVG inline para `HeartIcon`:
|
||||
```tsx
|
||||
function HeartIcon() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
strokeWidth={1.5} stroke="currentColor" className="size-5 shrink-0">
|
||||
<path strokeLinecap="round" strokeLinejoin="round"
|
||||
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
```
|
||||
Criar funções análogas: `ScaleIcon`, `CalendarIcon`, `UserCircleIcon`, `ArrowRightOnRectangleIcon`.
|
||||
|
||||
2. Substituir `navItems` por:
|
||||
```tsx
|
||||
const navItems = [
|
||||
{ to: '/area-do-cliente/favoritos', label: 'Favoritos', end: false, Icon: HeartIcon },
|
||||
{ to: '/area-do-cliente/comparar', label: 'Comparar', end: false, Icon: ScaleIcon },
|
||||
{ to: '/area-do-cliente/visitas', label: 'Visitas', end: false, Icon: CalendarIcon },
|
||||
{ to: '/area-do-cliente/conta', label: 'Minha conta', end: false, Icon: UserCircleIcon },
|
||||
];
|
||||
```
|
||||
Remover `adminNavItems` ou mantê-lo separado se o admin ainda precisar (não alterar funcionalidade admin).
|
||||
|
||||
3. No render de cada `NavLink` (sidebar e mobile), substituir `<span className="text-base">{item.icon}</span>` por `<item.Icon />`.
|
||||
|
||||
4. Substituir o botão de logout:
|
||||
```tsx
|
||||
{/* Antes */}
|
||||
<span>→</span>Sair
|
||||
{/* Depois */}
|
||||
<ArrowRightOnRectangleIcon />Sair
|
||||
```
|
||||
|
||||
5. **Mobile nav**: No bloco `lg:hidden`, os links agora têm 4 itens. Adicionar ícones SVG também na nav mobile e centralizar:
|
||||
```tsx
|
||||
<div className="flex flex-1 justify-center gap-1">
|
||||
{navItems.map(item => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
end={item.end}
|
||||
className={({ isActive }) =>
|
||||
`flex flex-col items-center gap-0.5 rounded-lg px-3 py-1.5 text-xs transition ${
|
||||
isActive
|
||||
? 'bg-surface text-textPrimary font-medium'
|
||||
: 'text-textSecondary hover:text-textPrimary'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<item.Icon />
|
||||
{item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] Menu lateral tem exatamente 4 itens: Favoritos, Comparar, Visitas, Minha conta.
|
||||
- [ ] "Painel" e "Boletos" não aparecem no menu.
|
||||
- [ ] Todos os ícones são SVG `<svg>` inline (sem emoji, sem texto Unicode).
|
||||
- [ ] Botão "Sair" usa `ArrowRightOnRectangleIcon`.
|
||||
- [ ] Item ativo está visualmente destacado em ambos: sidebar (desktop) e barra (mobile).
|
||||
- [ ] Mobile: 4 itens centralizados sem scroll horizontal.
|
||||
- [ ] Sem erros TypeScript/JSX no arquivo.
|
||||
|
||||
---
|
||||
|
||||
## Fase 6 — Frontend: Páginas
|
||||
|
||||
### T10 — Criar `ProfilePage.tsx`
|
||||
|
||||
**Arquivo**: `frontend/src/pages/client/ProfilePage.tsx` *(criar)*
|
||||
|
||||
**Contexto**: Página inexistente. Deve exibir dois formulários independentes: edição de nome e troca de senha.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Criar o arquivo com a seguinte estrutura:
|
||||
```tsx
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { changePassword, updateProfile } from '../../services/clientArea';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { user, updateUser } = useAuth();
|
||||
|
||||
// — Form de perfil —
|
||||
const [name, setName] = useState(user?.name ?? '');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const [nameSaving, setNameSaving] = useState(false);
|
||||
const [nameSuccess, setNameSuccess] = useState(false);
|
||||
|
||||
// — Form de senha —
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState('');
|
||||
const [passwordSaving, setPasswordSaving] = useState(false);
|
||||
const [passwordSuccess, setPasswordSuccess] = useState(false);
|
||||
|
||||
async function handleSaveName(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setNameError('');
|
||||
setNameSuccess(false);
|
||||
if (!name.trim()) {
|
||||
setNameError('O nome não pode ser vazio.');
|
||||
return;
|
||||
}
|
||||
setNameSaving(true);
|
||||
try {
|
||||
const updated = await updateProfile({ name: name.trim() });
|
||||
updateUser({ name: updated.name });
|
||||
setNameSuccess(true);
|
||||
} catch {
|
||||
setNameError('Erro ao salvar. Tente novamente.');
|
||||
} finally {
|
||||
setNameSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChangePassword(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setPasswordError('');
|
||||
setPasswordSuccess(false);
|
||||
if (newPassword.length < 8) {
|
||||
setPasswordError('A nova senha deve ter pelo menos 8 caracteres.');
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
setPasswordError('As senhas não coincidem.');
|
||||
return;
|
||||
}
|
||||
setPasswordSaving(true);
|
||||
try {
|
||||
await changePassword({ current_password: currentPassword, new_password: newPassword });
|
||||
setPasswordSuccess(true);
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.error ?? 'Erro ao alterar senha.';
|
||||
setPasswordError(msg);
|
||||
} finally {
|
||||
setPasswordSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-lg space-y-8">
|
||||
<h1 className="text-xl font-semibold text-textPrimary">Minha conta</h1>
|
||||
|
||||
{/* Formulário: dados pessoais */}
|
||||
<section className="rounded-xl border border-borderSubtle bg-panel p-6 space-y-4">
|
||||
<h2 className="text-sm font-semibold text-textPrimary">Dados pessoais</h2>
|
||||
<form onSubmit={handleSaveName} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs text-textSecondary mb-1">Nome</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
className="w-full rounded-lg border border-borderSubtle bg-canvas px-3 py-2 text-sm text-textPrimary focus:outline-none focus:border-brand"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-textSecondary mb-1">E-mail</label>
|
||||
<input
|
||||
type="email"
|
||||
value={user?.email ?? ''}
|
||||
readOnly
|
||||
className="w-full rounded-lg border border-borderSubtle bg-surface px-3 py-2 text-sm text-textTertiary cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
{nameError && <p className="text-xs text-red-400">{nameError}</p>}
|
||||
{nameSuccess && <p className="text-xs text-green-400">Nome atualizado com sucesso!</p>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={nameSaving}
|
||||
className="rounded-lg bg-brand px-4 py-2 text-sm font-medium text-white hover:bg-brandHover disabled:opacity-50 transition"
|
||||
>
|
||||
{nameSaving ? 'Salvando…' : 'Salvar alterações'}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{/* Formulário: trocar senha */}
|
||||
<section className="rounded-xl border border-borderSubtle bg-panel p-6 space-y-4">
|
||||
<h2 className="text-sm font-semibold text-textPrimary">Alterar senha</h2>
|
||||
<form onSubmit={handleChangePassword} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs text-textSecondary mb-1">Senha atual</label>
|
||||
<input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={e => setCurrentPassword(e.target.value)}
|
||||
className="w-full rounded-lg border border-borderSubtle bg-canvas px-3 py-2 text-sm text-textPrimary focus:outline-none focus:border-brand"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-textSecondary mb-1">Nova senha</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={e => setNewPassword(e.target.value)}
|
||||
className="w-full rounded-lg border border-borderSubtle bg-canvas px-3 py-2 text-sm text-textPrimary focus:outline-none focus:border-brand"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-textSecondary mb-1">Confirmar nova senha</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
className="w-full rounded-lg border border-borderSubtle bg-canvas px-3 py-2 text-sm text-textPrimary focus:outline-none focus:border-brand"
|
||||
/>
|
||||
</div>
|
||||
{passwordError && <p className="text-xs text-red-400">{passwordError}</p>}
|
||||
{passwordSuccess && <p className="text-xs text-green-400">Senha alterada com sucesso!</p>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={passwordSaving}
|
||||
className="rounded-lg bg-brand px-4 py-2 text-sm font-medium text-white hover:bg-brandHover disabled:opacity-50 transition"
|
||||
>
|
||||
{passwordSaving ? 'Salvando…' : 'Alterar senha'}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] Página renderiza sem erros.
|
||||
- [ ] Nome atual do usuário aparece pré-preenchido no campo "Nome".
|
||||
- [ ] E-mail aparece em campo `readOnly` (não editável).
|
||||
- [ ] Submit com nome vazio exibe erro inline sem chamar o servidor.
|
||||
- [ ] Submit bem-sucedido de nome exibe "Nome atualizado com sucesso!" e o sidebar reflete o novo nome.
|
||||
- [ ] Submit com `newPassword !== confirmPassword` exibe "As senhas não coincidem." sem chamar o servidor.
|
||||
- [ ] Submit com `newPassword.length < 8` exibe erro inline.
|
||||
- [ ] Senha atual incorreta → backend retorna `400` → frontend exibe "Senha atual incorreta".
|
||||
- [ ] Após troca de senha bem-sucedida, campos de senha são limpos.
|
||||
|
||||
---
|
||||
|
||||
### T11 — Melhorar `FavoritesPage.tsx` com cards enriquecidos
|
||||
|
||||
**Arquivo**: `frontend/src/pages/client/FavoritesPage.tsx`
|
||||
|
||||
**Contexto**: Cards atuais mostram só título e link. Com T01/T02, o backend já entrega `cover_photo_url`, `price`, `city`, `neighborhood`. Precisa exibir essas informações e melhorar o empty state.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Atualizar o import de tipos:
|
||||
```typescript
|
||||
import type { SavedProperty } from '../../types/clientArea';
|
||||
```
|
||||
2. Corrigir o tipo do estado: `useState<SavedProperty[]>([])`.
|
||||
3. Remover o componente `HeartButton` deste contexto — substituir pela ação "Remover dos favoritos" usando `removeFavorite` do serviço:
|
||||
```typescript
|
||||
import { getFavorites, removeFavorite } from '../../services/clientArea';
|
||||
```
|
||||
4. Implementar a função de remoção (optimistic update):
|
||||
```typescript
|
||||
async function handleRemove(savedId: string, propertyId: string | null) {
|
||||
if (!propertyId) return;
|
||||
setFavorites(prev => prev.filter(f => f.id !== savedId));
|
||||
try {
|
||||
await removeFavorite(propertyId);
|
||||
} catch {
|
||||
// Recarregar em caso de erro
|
||||
getFavorites().then(data => setFavorites(Array.isArray(data) ? data : []));
|
||||
}
|
||||
}
|
||||
```
|
||||
5. Substituir os cards no retorno JSX por:
|
||||
```tsx
|
||||
{favorites.map((item) => {
|
||||
const prop = item.property;
|
||||
const price = prop?.price
|
||||
? new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 })
|
||||
.format(parseFloat(prop.price))
|
||||
: null;
|
||||
const location = [prop?.neighborhood, prop?.city].filter(Boolean).join(', ');
|
||||
|
||||
return (
|
||||
<div key={item.id} className="rounded-xl border border-borderSubtle bg-panel overflow-hidden hover:border-borderStandard transition">
|
||||
{/* Thumbnail */}
|
||||
<div className="relative h-40 bg-surface">
|
||||
{prop?.cover_photo_url ? (
|
||||
<img
|
||||
src={prop.cover_photo_url}
|
||||
alt={prop.title}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-textQuaternary text-xs">
|
||||
Sem foto
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Info */}
|
||||
<div className="p-4">
|
||||
<p className="text-sm font-medium text-textPrimary line-clamp-2">{prop?.title ?? 'Imóvel'}</p>
|
||||
{price && <p className="mt-1 text-sm font-semibold text-brand">{price}</p>}
|
||||
{location && <p className="mt-0.5 text-xs text-textTertiary">{location}</p>}
|
||||
{/* Ações */}
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<a
|
||||
href={prop?.slug ? `/imoveis/${prop.slug}` : '#'}
|
||||
className="flex-1 rounded-lg border border-borderSubtle px-3 py-1.5 text-center text-xs text-textSecondary hover:text-textPrimary hover:border-borderStandard transition"
|
||||
>
|
||||
Ver imóvel
|
||||
</a>
|
||||
<button
|
||||
onClick={() => handleRemove(item.id, item.property_id)}
|
||||
className="rounded-lg border border-borderSubtle px-3 py-1.5 text-xs text-red-400 hover:bg-red-500/10 hover:border-red-500/20 transition"
|
||||
>
|
||||
Remover
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] Cada card exibe: thumbnail (ou placeholder "Sem foto"), título, preço formatado em BRL, cidade/bairro.
|
||||
- [ ] Botão "Remover" remove o card da lista imediatamente (optimistic update) e chama `removeFavorite`.
|
||||
- [ ] Botão "Ver imóvel" navega para `/imoveis/<slug>`.
|
||||
- [ ] Empty state exibe link para `/imoveis`.
|
||||
- [ ] Sem erros TypeScript.
|
||||
|
||||
---
|
||||
|
||||
### T12 — Melhorar `VisitsPage.tsx` com cancelamento de visita
|
||||
|
||||
**Arquivo**: `frontend/src/pages/client/VisitsPage.tsx`
|
||||
|
||||
**Contexto**: Página só exibe visitas. Precisa adicionar botão "Cancelar" para `status=pending`, com confirmação simples e optimistic update.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Adicionar import do serviço:
|
||||
```typescript
|
||||
import { cancelVisit, getVisits } from '../../services/clientArea';
|
||||
```
|
||||
2. Adicionar estado para controle de cancelamento:
|
||||
```typescript
|
||||
const [cancelling, setCancelling] = useState<string | null>(null); // visitId em progresso
|
||||
const [cancelError, setCancelError] = useState<string | null>(null);
|
||||
```
|
||||
3. Implementar `handleCancel`:
|
||||
```typescript
|
||||
async function handleCancel(visitId: string) {
|
||||
if (!window.confirm('Confirmar cancelamento desta visita?')) return;
|
||||
setCancelling(visitId);
|
||||
setCancelError(null);
|
||||
// Optimistic update
|
||||
setVisits(prev =>
|
||||
prev.map(v => (v.id === visitId ? { ...v, status: 'cancelled' as const } : v))
|
||||
);
|
||||
try {
|
||||
const updated = await cancelVisit(visitId);
|
||||
setVisits(prev => prev.map(v => (v.id === visitId ? updated : v)));
|
||||
} catch (err: any) {
|
||||
// Reverter
|
||||
setCancelError('Não foi possível cancelar. Tente novamente.');
|
||||
getVisits().then(setVisits).catch(() => {});
|
||||
} finally {
|
||||
setCancelling(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
4. Dentro do map de visitas, após o badge de status, adicionar o botão condicional:
|
||||
```tsx
|
||||
{visit.status === 'pending' && (
|
||||
<button
|
||||
onClick={() => handleCancel(visit.id)}
|
||||
disabled={cancelling === visit.id}
|
||||
className="mt-3 rounded-lg border border-red-500/30 px-3 py-1.5 text-xs text-red-400 hover:bg-red-500/10 disabled:opacity-50 transition"
|
||||
>
|
||||
{cancelling === visit.id ? 'Cancelando…' : 'Cancelar visita'}
|
||||
</button>
|
||||
)}
|
||||
```
|
||||
5. Exibir `cancelError` se presente, logo após o bloco de visitas:
|
||||
```tsx
|
||||
{cancelError && (
|
||||
<p className="mt-2 text-xs text-red-400">{cancelError}</p>
|
||||
)}
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] Visita com `status=pending` exibe botão "Cancelar visita".
|
||||
- [ ] Ao clicar, `window.confirm` é exibido; se cancelado pelo usuário, nenhuma ação.
|
||||
- [ ] Ao confirmar, o badge de status muda imediatamente para "Cancelada" (optimistic).
|
||||
- [ ] O botão "Cancelar visita" desaparece após status mudar.
|
||||
- [ ] Visitas com status diferente de `pending` não exibem o botão.
|
||||
- [ ] Em caso de erro de rede, mensagem de erro é exibida e lista é recarregada.
|
||||
|
||||
---
|
||||
|
||||
### T13 — Melhorar empty state de `ComparisonPage.tsx`
|
||||
|
||||
**Arquivo**: `frontend/src/pages/client/ComparisonPage.tsx`
|
||||
|
||||
**Contexto**: O empty state atual diz "Nenhum imóvel selecionado para comparação" com link para `/imoveis`. Falta instrução clara de como usar a feature.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Localizar o bloco do empty state (quando `properties.length === 0`).
|
||||
2. Substituir o conteúdo interno do `div` de empty state por:
|
||||
```tsx
|
||||
<div className="rounded-xl border border-borderSubtle bg-panel p-12 text-center max-w-sm mx-auto">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-surface">
|
||||
{/* ScaleIcon SVG inline */}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
strokeWidth={1.5} stroke="currentColor" className="size-6 text-textTertiary">
|
||||
<path strokeLinecap="round" strokeLinejoin="round"
|
||||
d="M12 3v17.25m0 0c-1.472 0-2.882.265-4.185.75M12 20.25c1.472 0 2.882.265 4.185.75M18.75 4.97A48.416 48.416 0 0 0 12 4.5c-2.291 0-4.545.16-6.75.47m13.5 0c1.01.143 2.01.317 3 .52m-3-.52 2.62 15.95M5.25 4.97l-2.62 15.95m0 0a48.959 48.959 0 0 0 3.32.65M5.63 20.92a48.958 48.958 0 0 0 3.32-.65m9.63.65a48.952 48.952 0 0 0 3.32-.65" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-textPrimary mb-2">Nenhum imóvel para comparar</p>
|
||||
<p className="text-xs text-textTertiary mb-4">
|
||||
Acesse um imóvel no catálogo e clique em{' '}
|
||||
<span className="font-medium text-textSecondary">"Comparar"</span>{' '}
|
||||
para adicioná-lo aqui. Você pode comparar até 3 imóveis lado a lado.
|
||||
</p>
|
||||
<Link
|
||||
to="/imoveis"
|
||||
className="inline-block rounded-lg bg-brand px-4 py-2 text-xs font-medium text-white hover:bg-brandHover transition"
|
||||
>
|
||||
Explorar imóveis
|
||||
</Link>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] Empty state exibe instrução explicando como adicionar imóveis à comparação.
|
||||
- [ ] Instrução menciona o botão "Comparar" nos cards de imóvel.
|
||||
- [ ] Link "Explorar imóveis" navega para `/imoveis`.
|
||||
- [ ] Quando há imóveis na comparação, a tabela é exibida normalmente (sem regressão).
|
||||
|
||||
---
|
||||
|
||||
## Fase 7 — Frontend: Roteamento
|
||||
|
||||
### T14 — Atualizar rotas em `App.tsx`
|
||||
|
||||
**Arquivo**: `frontend/src/App.tsx`
|
||||
|
||||
**Contexto**: Precisa: (a) redirecionar `/area-do-cliente` para `/area-do-cliente/favoritos`; (b) remover a rota `/boletos`; (c) adicionar a rota `/conta` apontando para `ProfilePage`.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Adicionar import de `Navigate` e `ProfilePage`:
|
||||
```typescript
|
||||
import { Navigate } from 'react-router-dom'; // adicionar ao import existente de react-router-dom
|
||||
import ProfilePage from './pages/client/ProfilePage';
|
||||
```
|
||||
2. Remover os imports de `BoletosPage` e `ClientDashboardPage`:
|
||||
```typescript
|
||||
// Remover estas linhas:
|
||||
import BoletosPage from './pages/client/BoletosPage';
|
||||
import ClientDashboardPage from './pages/client/ClientDashboardPage';
|
||||
```
|
||||
3. Localizar o bloco de rotas da área do cliente e substituir:
|
||||
```tsx
|
||||
{/* Antes */}
|
||||
<Route index element={<ClientDashboardPage />} />
|
||||
<Route path="favoritos" element={<FavoritesPage />} />
|
||||
<Route path="comparar" element={<ComparisonPage />} />
|
||||
<Route path="visitas" element={<VisitsPage />} />
|
||||
<Route path="boletos" element={<BoletosPage />} />
|
||||
|
||||
{/* Depois */}
|
||||
<Route index element={<Navigate to="favoritos" replace />} />
|
||||
<Route path="favoritos" element={<FavoritesPage />} />
|
||||
<Route path="comparar" element={<ComparisonPage />} />
|
||||
<Route path="visitas" element={<VisitsPage />} />
|
||||
<Route path="conta" element={<ProfilePage />} />
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] Acessar `/area-do-cliente` redireciona para `/area-do-cliente/favoritos` (Replace — sem entrada no histórico).
|
||||
- [ ] A rota `/area-do-cliente/boletos` não existe mais (retorna 404 do React Router).
|
||||
- [ ] A rota `/area-do-cliente/conta` renderiza `ProfilePage`.
|
||||
- [ ] Sem erros TypeScript/lint no arquivo.
|
||||
|
||||
---
|
||||
|
||||
## Fase 8 — Remoção de Arquivos Obsoletos
|
||||
|
||||
### T15 — Deletar `BoletosPage.tsx` e `ClientDashboardPage.tsx`
|
||||
|
||||
**Arquivos a deletar**:
|
||||
- `frontend/src/pages/client/BoletosPage.tsx`
|
||||
- `frontend/src/pages/client/ClientDashboardPage.tsx`
|
||||
|
||||
**Contexto**: Após T14, nenhum import desses componentes existe mais no projeto. Removê-los evita confusão futura.
|
||||
|
||||
**Passos**:
|
||||
|
||||
1. Verificar que nenhum arquivo do projeto importa `BoletosPage` ou `ClientDashboardPage`:
|
||||
```powershell
|
||||
Select-String -Path "frontend/src/**" -Pattern "BoletosPage|ClientDashboardPage" -Recurse
|
||||
```
|
||||
2. Se o resultado for vazio (apenas os próprios arquivos), deletar:
|
||||
```powershell
|
||||
Remove-Item "frontend/src/pages/client/BoletosPage.tsx"
|
||||
Remove-Item "frontend/src/pages/client/ClientDashboardPage.tsx"
|
||||
```
|
||||
|
||||
**Critérios de conclusão**:
|
||||
- [ ] Os dois arquivos não existem mais no projeto.
|
||||
- [ ] Nenhum arquivo do projeto os importa.
|
||||
- [ ] Build do frontend (ou `tsc --noEmit`) passa sem erros.
|
||||
|
||||
---
|
||||
|
||||
## Grafo de Dependências
|
||||
|
||||
```
|
||||
T01 (Schemas backend)
|
||||
└─ T02 (Eager load GET /favorites) → T11 (FavoritesPage)
|
||||
└─ T03 (PATCH /profile) → T07 (cancelVisit/updateProfile) → T10 (ProfilePage)
|
||||
└─ T04 (PATCH /password) ↗
|
||||
└─ T05 (PATCH /visits/cancel) → T07 (cancelVisit) → T12 (VisitsPage)
|
||||
|
||||
T06 (Tipos TS) → T07 (Serviços) → T10, T11, T12
|
||||
T08 (AuthContext.updateUser) → T10 (ProfilePage usa updateUser)
|
||||
T09 (ClientLayout) — independente, pode ser feito em paralelo com T01–T08
|
||||
T13 (ComparisonPage empty state) — independente
|
||||
T14 (App.tsx rotas) → depende de T10 (ProfilePage deve existir)
|
||||
T15 (Deletar arquivos) → depende de T14
|
||||
```
|
||||
|
||||
## Execução em Paralelo
|
||||
|
||||
| Grupo paralelo | Tasks |
|
||||
|---|---|
|
||||
| 1 (backend) | T01 → (T02, T03, T04, T05) em paralelo após T01 |
|
||||
| 2 (frontend infra) | T06, T08, T09, T13 podem ser feitos em paralelo entre si |
|
||||
| 3 (serviços) | T07 após T06 |
|
||||
| 4 (páginas) | T10 após T03+T04+T08+T07; T11 após T02+T06+T07; T12 após T05+T06+T07 |
|
||||
| 5 (finalização) | T14 após T10; T15 após T14 |
|
||||
|
||||
## Checklist Resumida
|
||||
|
||||
- [X] T01 [P] Schemas backend: `PropertyCard`, `UpdateProfileIn/Out`, `UpdatePasswordIn` em `backend/app/schemas/client_area.py`
|
||||
- [X] T12 [US3] Cancelamento em `frontend/src/pages/client/VisitsPage.tsx`
|
||||
- [X] T13 [P] [US6] Empty state em `frontend/src/pages/client/ComparisonPage.tsx`
|
||||
- [X] T14 Rotas em `frontend/src/App.tsx`
|
||||
- [X] T15 Deletar `BoletosPage.tsx` e `ClientDashboardPage.tsx`
|
||||
173
specs/029-ux-area-do-cliente/ux-audit.md
Normal file
173
specs/029-ux-area-do-cliente/ux-audit.md
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# UX/UI Audit — Área do Cliente (`/area-do-cliente`)
|
||||
|
||||
**Data:** 2026-04-22
|
||||
**Auditor:** GitHub Copilot (UX/UI Review)
|
||||
**Escopo:** Todas as páginas e componentes sob `/area-do-cliente`
|
||||
|
||||
---
|
||||
|
||||
## 1. Inventário de Telas Atuais
|
||||
|
||||
| Rota | Componente | Status |
|
||||
|------|-----------|--------|
|
||||
| `/area-do-cliente` | `ClientDashboardPage` | ❌ Remover |
|
||||
| `/area-do-cliente/favoritos` | `FavoritesPage` | 🔧 Melhorar |
|
||||
| `/area-do-cliente/comparar` | `ComparisonPage` | 🔧 Melhorar |
|
||||
| `/area-do-cliente/visitas` | `VisitsPage` | 🔧 Melhorar |
|
||||
| `/area-do-cliente/boletos` | `BoletosPage` | ❌ Remover |
|
||||
|
||||
---
|
||||
|
||||
## 2. Problemas Identificados
|
||||
|
||||
### 2.1 Dashboard (Painel) — REMOVER
|
||||
- **Problema:** 3 cards com números (favoritos, visitas, boletos). Não agrega valor real ao usuário — ele precisa clicar de qualquer forma para ver os detalhes.
|
||||
- **Impacto:** UX: Alto. Adiciona um clique desnecessário para chegar ao conteúdo real.
|
||||
- **Decisão:** Remover o dashboard. Redirecionar `/area-do-cliente` → `/area-do-cliente/favoritos`.
|
||||
|
||||
### 2.2 Boletos — REMOVER
|
||||
- **Problema:** Funcionalidade de boletos é uma feature de gestão imobiliária avançada raramente usada. A tabela existe mas os dados dependem de inserção manual pelo admin. Gera confusão para usuários sem boletos.
|
||||
- **Impacto:** UX: Médio. Ruído visual no menu, expectativa não cumprida.
|
||||
- **Decisão:** Remover a rota, o componente e o item do menu. Manter o backend (não excluir o model/API).
|
||||
|
||||
### 2.3 Navegação (Sidebar) — Ícones
|
||||
- **Problema:** Ícones usam caracteres Unicode/emoji (`⊞`, `♡`, `⇄`, `📅`, `📄`) que rendem de forma inconsistente entre sistemas operacionais e navegadores. Look não-profissional em temas dark/light.
|
||||
- **Impacto:** Visual: Alto. Inconsistência de rendering cross-platform.
|
||||
- **Decisão:** Substituir por SVG inline (Heroicons 2.0 outline) para cada item de nav.
|
||||
|
||||
### 2.4 FavoritesPage — Cards sem imagem/preço
|
||||
- **Problema:** Cards de favoritos mostram apenas título e link "Ver detalhes →". O usuário não consegue lembrar qual imóvel é qual sem clicar.
|
||||
- **Impacto:** UX: Alto. Experiência frustrante para quem tem múltiplos favoritos.
|
||||
- **Decisão:** Mostrar thumbnail da primeira foto, preço e cidade/bairro no card.
|
||||
|
||||
### 2.5 FavoritesPage — Layout de grid muito esparso
|
||||
- **Problema:** Grid `1 → 2 → 3 colunas` com cards muito altos (h-48 skeleton) para conteúdo mínimo.
|
||||
- **Decisão:** Cards com imagem real + info resumida, altura proporcional.
|
||||
|
||||
### 2.6 VisitsPage — Sem ação do usuário
|
||||
- **Problema:** O usuário só visualiza visitas. Não há como cancelar, nem há indicação clara de próximos passos.
|
||||
- **Impacto:** UX: Médio. Usuário precisa entrar em contato por outro canal para cancelar.
|
||||
- **Decisão:** Adicionar botão "Solicitar cancelamento" para visitas com status `pending`.
|
||||
|
||||
### 2.7 VisitsPage — Layout de lista plana
|
||||
- **Problema:** Cards de visita não têm hierarquia visual clara. Data e status competem com o título.
|
||||
- **Decisão:** Refatorar card de visita com: título em destaque, data em destaque, status badge alinhado à direita, mensagem como texto secundário colapsável.
|
||||
|
||||
### 2.8 ComparisonPage — Empty state sem call-to-action útil
|
||||
- **Problema:** Empty state tem apenas "Nenhum imóvel selecionado" e link para `/imoveis`. Usuário não sabe como adicionar imóveis à comparação.
|
||||
- **Decisão:** Empty state explicativo com instrução de como usar a feature (tooltip/hint).
|
||||
|
||||
### 2.9 ClientLayout — Sidebar sem perfil editável
|
||||
- **Problema:** Sidebar mostra avatar + nome + email mas sem link para perfil/configurações.
|
||||
- **Impacto:** UX: Médio. Usuário não consegue alterar dados cadastrais pela área do cliente.
|
||||
- **Decisão:** Adicionar link "Minha conta" apontando para nova rota `/area-do-cliente/conta`.
|
||||
|
||||
### 2.10 ClientLayout — Botão "Sair" sem ícone próprio
|
||||
- **Problema:** Usa `→` como ícone de logout. Semântica errada (parece "ir para").
|
||||
- **Decisão:** Substituir por ícone ArrowRightOnRectangle (Heroicons logout).
|
||||
|
||||
### 2.11 Mobile Nav — Scroll horizontal fraco
|
||||
- **Problema:** Em mobile, a navegação é uma barra horizontal com scroll. Após remover boletos e painel, teremos 3 itens — cabe bem. Mas ainda carece de indicador visual claro de ativo.
|
||||
- **Decisão:** Melhorar o indicador ativo com underline/pill e alinhar os 3 itens centralizados.
|
||||
|
||||
### 2.12 Ausência de página de perfil/conta
|
||||
- **Problema:** Não existe `/area-do-cliente/conta`. Usuário não pode ver/editar nome, email ou senha.
|
||||
- **Decisão:** Criar `ProfilePage` com form de atualização de nome e alteração de senha.
|
||||
|
||||
---
|
||||
|
||||
## 3. Resumo das Decisões
|
||||
|
||||
### ❌ Remover
|
||||
- `ClientDashboardPage` (`/area-do-cliente`) — redirecionar para `/area-do-cliente/favoritos`
|
||||
- `BoletosPage` (`/area-do-cliente/boletos`) — remover rota, nav item e componente
|
||||
- Item "Boletos" e item "Painel" do nav
|
||||
|
||||
### ✅ Manter (com melhorias)
|
||||
- `FavoritesPage` — melhorar cards com thumbnail + preço + localização
|
||||
- `VisitsPage` — adicionar cancelamento + melhorar layout do card
|
||||
- `ComparisonPage` — melhorar empty state
|
||||
- `ClientLayout` — trocar ícones por SVG, adicionar link "Minha conta", melhorar logout
|
||||
|
||||
### ➕ Criar
|
||||
- `ProfilePage` (`/area-do-cliente/conta`) — formulário de edição de perfil
|
||||
|
||||
---
|
||||
|
||||
## 4. Nova Estrutura de Navegação
|
||||
|
||||
**Antes (5 itens + Painel):**
|
||||
```
|
||||
⊞ Painel
|
||||
♡ Favoritos
|
||||
⇄ Comparar
|
||||
📅 Visitas
|
||||
📄 Boletos
|
||||
```
|
||||
|
||||
**Depois (4 itens, sem Painel como item separado — é o redirect):**
|
||||
```
|
||||
[heart-icon] Favoritos
|
||||
[scale-icon] Comparar
|
||||
[calendar-icon] Visitas
|
||||
[user-icon] Minha conta
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Fluxo UX Revisado
|
||||
|
||||
```
|
||||
/area-do-cliente → redirect 302 → /area-do-cliente/favoritos
|
||||
|
||||
/area-do-cliente/favoritos
|
||||
Cards: thumbnail | título | preço | cidade
|
||||
Ação: [Remover dos favoritos] [Ver imóvel →]
|
||||
|
||||
/area-do-cliente/visitas
|
||||
Cards: título imóvel | data | status badge
|
||||
Ação: [Solicitar cancelamento] (só para status=pending)
|
||||
|
||||
/area-do-cliente/comparar
|
||||
Tabela: até 3 imóveis lado a lado
|
||||
Empty state: instrução de como adicionar imóvel à comparação
|
||||
|
||||
/area-do-cliente/conta
|
||||
Form: Nome | Email (readonly) | Senha atual | Nova senha | Confirmar senha
|
||||
Ação: [Salvar alterações]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Componentes Afetados
|
||||
|
||||
| Arquivo | Ação |
|
||||
|---------|------|
|
||||
| `frontend/src/layouts/ClientLayout.tsx` | Refatorar nav (SVG icons, remover Boletos/Painel, adicionar Conta) |
|
||||
| `frontend/src/pages/client/ClientDashboardPage.tsx` | Deletar ou converter em redirect |
|
||||
| `frontend/src/pages/client/BoletosPage.tsx` | Deletar |
|
||||
| `frontend/src/pages/client/FavoritesPage.tsx` | Melhorar cards |
|
||||
| `frontend/src/pages/client/VisitsPage.tsx` | Melhorar card + adicionar cancelamento |
|
||||
| `frontend/src/pages/client/ComparisonPage.tsx` | Melhorar empty state |
|
||||
| `frontend/src/pages/client/ProfilePage.tsx` | Criar (novo) |
|
||||
| `frontend/src/services/clientArea.ts` | Adicionar `updateProfile`, `changePassword`, `cancelVisit` |
|
||||
| `frontend/src/App.tsx` | Atualizar rotas (remover boletos, adicionar conta, redirect) |
|
||||
| `backend/app/routes/client.py` | Adicionar PATCH `/me/profile`, PATCH `/me/password`, PATCH `/me/visits/:id/cancel` |
|
||||
| `backend/app/schemas/client.py` | Adicionar schemas de update |
|
||||
|
||||
---
|
||||
|
||||
## 7. Critérios de Aceite
|
||||
|
||||
- [ ] `/area-do-cliente` redireciona para `/area-do-cliente/favoritos`
|
||||
- [ ] "Boletos" não aparece no menu e a rota 404
|
||||
- [ ] "Painel" não aparece no menu
|
||||
- [ ] Ícones do menu são SVG Heroicons consistentes em dark e light
|
||||
- [ ] Card de favorito mostra: imagem (ou placeholder), título, preço, cidade
|
||||
- [ ] Card de visita mostra: título, data agendada (ou solicitada), status badge
|
||||
- [ ] Visita com `status=pending` exibe botão "Cancelar" que muda para `cancelled`
|
||||
- [ ] Empty state do comparar explica como usar a feature
|
||||
- [ ] `/area-do-cliente/conta` exibe form com nome e troca de senha
|
||||
- [ ] Form de conta valida: nome obrigatório, senhas com mínimo 8 chars, confirmação igual
|
||||
- [ ] Mobile nav centraliza os 4 itens sem scroll (ou scroll suave se necessário)
|
||||
- [ ] Botão "Sair" usa ícone de logout correto
|
||||
Loading…
Add table
Add a link
Reference in a new issue