# 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//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//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//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 { const response = await api.patch('/me/profile', data); return response.data; } export async function changePassword(data: ChangePasswordPayload): Promise { await api.patch('/me/password', data); } export async function cancelVisit(visitId: string): Promise { const response = await api.patch(`/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`, `cancelVisit` retorna `Promise`. - [ ] 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 register: (data: RegisterCredentials) => Promise logout: () => void updateUser: (partial: Partial) => void // NOVO } ``` 2. Implementar `updateUser` dentro do `AuthProvider`, após a declaração de `logout`: ```typescript const updateUser = useCallback((partial: Partial) => { 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 ( ); } ``` 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 `{item.icon}` por ``. 4. Substituir o botão de logout: ```tsx {/* Antes */} Sair {/* Depois */} 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
{navItems.map(item => ( `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.label} ))}
``` **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 `` 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 (

Minha conta

{/* Formulário: dados pessoais */}

Dados pessoais

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" />
{nameError &&

{nameError}

} {nameSuccess &&

Nome atualizado com sucesso!

}
{/* Formulário: trocar senha */}

Alterar senha

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" />
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" />
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" />
{passwordError &&

{passwordError}

} {passwordSuccess &&

Senha alterada com sucesso!

}
); } ``` **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([])`. 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 (
{/* Thumbnail */}
{prop?.cover_photo_url ? ( {prop.title} ) : (
Sem foto
)}
{/* Info */}

{prop?.title ?? 'Imóvel'}

{price &&

{price}

} {location &&

{location}

} {/* Ações */}
Ver imóvel
); })} ``` **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/`. - [ ] 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(null); // visitId em progresso const [cancelError, setCancelError] = useState(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' && ( )} ``` 5. Exibir `cancelError` se presente, logo após o bloco de visitas: ```tsx {cancelError && (

{cancelError}

)} ``` **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
{/* ScaleIcon SVG inline */}

Nenhum imóvel para comparar

Acesse um imóvel no catálogo e clique em{' '} "Comparar"{' '} para adicioná-lo aqui. Você pode comparar até 3 imóveis lado a lado.

Explorar imóveis
``` **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 */} } /> } /> } /> } /> } /> {/* Depois */} } /> } /> } /> } /> } /> ``` **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`