sass-imobiliaria/.specify/features/011-enrich-client-profile/plan.md

14 KiB

Implementation Plan: Enriquecimento do Perfil de Cliente

Branch: 011-enrich-client-profile | Date: 2026-04-14 | Spec: spec.md Depends On: Feature 005 — ClientUser model base; Feature 007 — admin panel routes/pages

Summary

Expansão do perfil de cliente com 12 novas colunas opcionais na tabela client_users (contato, dados pessoais, endereço, observações). Reescrita completa de AdminClientesPage.tsx (tabela rica com avatar, busca, links de contato) e ClienteForm.tsx (formulário full-screen com 5 seções e máscaras). Backend recebe migration Alembic, atualização do model SQLAlchemy e novos schemas Pydantic, além de atualização dos handlers de criação/edição.

Technical Context

Language/Version: Python 3.12 (backend) · TypeScript 5.5 (frontend) Primary Dependencies: Flask 3.x, SQLAlchemy 2.x, Pydantic v2 (backend) · React 18, Tailwind CSS 3.4, react-router-dom v6, Axios (frontend) Storage: PostgreSQL 16 — 12 novas colunas nullable em client_users Testing: pytest (backend) — testes de rotas admin ; Vite build check (frontend) Target Platform: Servidor Linux (Docker) + SPA na mesma origem via proxy Vite Project Type: Web service (Flask REST API) + SPA (React) Performance Goals: Listagem de clientes renderizada < 1 s; busca local (sem debounce de rede) < 100 ms Constraints: Todos os 12 campos novos são opcionais (nullable no DB, Optional no schema); sem validação de CPF por dígito verificador no MVP; máscaras são apenas visuais (frontend-only) Scale/Scope: MVP — dados pessoais e endereço completos; sem integração com correios/CEP lookup

Constitution Check

Princípio Status Observação
I. Design-First PASS Avatar com iniciais usa fundo #5e6ad2; tabela usa tokens de cor dark existentes; formulário segue padrão PropertyForm.
II. Separation of Concerns PASS Backend retorna JSON puro. Máscaras são responsabilidade exclusiva do frontend.
III. Spec-Driven PASS spec.md aprovado → plan.md (este) → tasks.md → implementação.
IV. Data Integrity PASS 12 colunas nullable; entrada validada via Pydantic antes de persistir. cpf armazenado sem formatação (apenas dígitos) — máscara é aplicada só na UI.
V. Security PASS Endpoints /api/v1/admin/clientes/* já protegidos por require_auth + verificação de role admin. Nenhuma rota nova pública.
VI. Simplicity First PASS Busca filtrada no frontend (sem endpoint de search). Sem lookup de CEP externo. Sem validação de dígito verificador de CPF.

POST-DESIGN RE-CHECK: A adição de 12 colunas nullable não introduz lógica de negócio nova. Reescrita de components é cirúrgica — apenas dois arquivos de página/formulário.

Architecture Overview

Migration Alembic
    └── adiciona 12 colunas nullable em client_users
          │
          ▼
SQLAlchemy Model (user.py)  ◄── já contém as 12 colunas (model estava adiantado)
          │
          ▼
Pydantic Schemas (auth.py ou novo admin_clients.py)
    ├── ClientUserOut          — leitura (inclui todos os campos)
    ├── ClientUserCreateIn     — criação (campos obrigatórios + 12 opcionais)
    └── ClientUserUpdateIn     — edição (todos Optional)
          │
          ▼
Route Handlers (admin.py)
    ├── GET  /api/v1/admin/clientes          — lista com novos campos
    ├── POST /api/v1/admin/clientes          — cria com novos campos
    └── PUT  /api/v1/admin/clientes/<id>     — atualiza com novos campos
          │
          ▼
Frontend Services (adminService.ts ou clienteService.ts)
          │
          ▼
AdminClientesPage.tsx  ◄── reescrita: tabela rica + busca + avatar
ClienteForm.tsx        ◄── reescrita: full-screen + 5 seções + máscaras

Database Changes

Migration: xxxx_enrich_client_users_profile.py

  • Arquivo: backend/migrations/versions/xxxx_enrich_client_users_profile.py
  • revision: gerado automaticamente pelo Alembic
  • down_revision: f1a2b3c4d5e6 (último migration existente: add_iptu_anual)
  • Estratégia: op.batch_alter_table para compatibilidade SQLite em testes

Colunas adicionadas (todas nullable=True):

Coluna Tipo SQLAlchemy Tamanho Observação
phone String 20 Telefone fixo ou celular
whatsapp String 20 Número WhatsApp
cpf String 14 Armazenado sem máscara (11 dígitos)
birth_date Date Data de nascimento
address_street String 200 Logradouro
address_number String 20 Número
address_complement String 100 Complemento
address_neighborhood String 100 Bairro
address_city String 100 Cidade
address_state String 2 UF (2 chars)
address_zip String 9 CEP com hífen (8 dígitos)
notes Text Observações internas do admin

Upgrade:

def upgrade():
    with op.batch_alter_table('client_users', schema=None) as batch_op:
        batch_op.add_column(sa.Column('phone', sa.String(20), nullable=True))
        batch_op.add_column(sa.Column('whatsapp', sa.String(20), nullable=True))
        batch_op.add_column(sa.Column('cpf', sa.String(14), nullable=True))
        batch_op.add_column(sa.Column('birth_date', sa.Date(), nullable=True))
        batch_op.add_column(sa.Column('address_street', sa.String(200), nullable=True))
        batch_op.add_column(sa.Column('address_number', sa.String(20), nullable=True))
        batch_op.add_column(sa.Column('address_complement', sa.String(100), nullable=True))
        batch_op.add_column(sa.Column('address_neighborhood', sa.String(100), nullable=True))
        batch_op.add_column(sa.Column('address_city', sa.String(100), nullable=True))
        batch_op.add_column(sa.Column('address_state', sa.String(2), nullable=True))
        batch_op.add_column(sa.Column('address_zip', sa.String(9), nullable=True))
        batch_op.add_column(sa.Column('notes', sa.Text(), nullable=True))

Downgrade: batch_op.drop_column para cada coluna na ordem inversa.

Nota: O model ClientUser em backend/app/models/user.py já possui as 12 colunas declaradas (adiantado manualmente). A migration é necessária para sincronizar o banco existente.

Backend Changes

1. SQLAlchemy Model (backend/app/models/user.py)

Status: Já implementado — as 12 colunas estão declaradas no model atual. Nenhuma alteração necessária; apenas a migration precisa ser executada.

2. Pydantic Schemas

Arquivo: backend/app/schemas/auth.py (atualizar) ou extrair para backend/app/schemas/admin_clients.py

ClientUserOut

class ClientUserOut(BaseModel):
    id: str
    name: str
    email: str
    role: str
    created_at: datetime
    # Contato
    phone: Optional[str] = None
    whatsapp: Optional[str] = None
    # Dados pessoais
    cpf: Optional[str] = None
    birth_date: Optional[date] = None
    # Endereço
    address_street: Optional[str] = None
    address_number: Optional[str] = None
    address_complement: Optional[str] = None
    address_neighborhood: Optional[str] = None
    address_city: Optional[str] = None
    address_state: Optional[str] = None
    address_zip: Optional[str] = None
    # Observações
    notes: Optional[str] = None

    model_config = {"from_attributes": True}

ClientUserCreateIn

class ClientUserCreateIn(BaseModel):
    name: str
    email: EmailStr
    password: str
    role: str = "client"
    # 12 campos opcionais (mesmos tipos de ClientUserOut)
    phone: Optional[str] = None
    # ... demais 11 campos

ClientUserUpdateIn

class ClientUserUpdateIn(BaseModel):
    name: Optional[str] = None
    email: Optional[EmailStr] = None
    password: Optional[str] = None
    role: Optional[str] = None
    # 12 campos opcionais
    phone: Optional[str] = None
    # ... demais 11 campos

3. Route Handlers (backend/app/routes/admin.py)

Endpoints já existem. Alterações necessárias:

Endpoint Ação Mudança
GET /api/v1/admin/clientes Listar Substituir UserOutClientUserOut na serialização
POST /api/v1/admin/clientes Criar Validar com ClientUserCreateIn; persistir os 12 novos campos
PUT /api/v1/admin/clientes/<id> Editar Validar com ClientUserUpdateIn; atualizar campos presentes (ignorar None)

Padrão de update (apenas campos enviados):

data = ClientUserUpdateIn(**request.get_json())
for field, value in data.model_dump(exclude_none=True).items():
    if field == "password":
        setattr(user, "password_hash", bcrypt.hashpw(...))
    else:
        setattr(user, field, value)

Frontend Changes

1. Types (frontend/src/types/)

Arquivo: frontend/src/types/clientUser.ts (novo) ou atualizar frontend/src/types/index.ts

export interface ClientUser {
  id: string;
  name: string;
  email: string;
  role: string;
  created_at: string;
  phone?: string;
  whatsapp?: string;
  cpf?: string;
  birth_date?: string;
  address_street?: string;
  address_number?: string;
  address_complement?: string;
  address_neighborhood?: string;
  address_city?: string;
  address_state?: string;
  address_zip?: string;
  notes?: string;
}

2. AdminClientesPage.tsx — Reescrita completa

Arquivo: frontend/src/pages/admin/AdminClientesPage.tsx

Funcionalidades:

  • useState para lista de clientes, loading, erro, termo de busca, cliente selecionado, modo (list | form)
  • useEffectGET /api/v1/admin/clientes ao montar
  • Avatar com iniciais: div circular com fundo #5e6ad2, iniciais em uppercase
  • Barra de busca: input controlado que filtra filteredClientes em memória por name, email, phone, cpf
  • Tabela responsiva:
    • Mobile: nome, avatar, e-mail, ações
    • Desktop: + telefone (tel: link), WhatsApp (ícone abre https://wa.me/55{numero}), CPF, endereço resumido (bairro, cidade/UF), tipo, cadastro
  • Ações por linha: botão Editar → abre ClienteForm; botão Excluir → DELETE + confirm
  • Botão "Novo Cliente" abre ClienteForm sem clienteId

3. ClienteForm.tsx — Reescrita completa

Arquivo: frontend/src/pages/admin/ClienteForm.tsx

Layout: Full-screen (mesmo padrão de PropertyForm.tsx), com header fixo contendo título e botões Cancelar/Salvar.

5 Seções:

Seção Campos
Dados Pessoais Nome*, CPF (máscara 000.000.000-00), Data de Nascimento
Contato Telefone (máscara (00) 00000-0000), WhatsApp (máscara (00) 00000-0000)
Endereço CEP (máscara 00000-000), Logradouro, Número, Complemento, Bairro, Cidade, Estado (select UF)
Acesso E-mail*, Senha (obrigatória no create, opcional no edit), Tipo (client / admin)
Observações textarea para notas internas

Máscaras (implementação manual via onChange, sem lib externa):

  • CPF: ###.###.###-##
  • Telefone/WhatsApp: (##) #####-####
  • CEP: #####-###

Submit:

  • Criação: POST /api/v1/admin/clientes com ClientUserCreateIn
  • Edição: PUT /api/v1/admin/clientes/{id} com ClientUserUpdateIn (apenas campos alterados)
  • Senha omitida no payload de edição se campo vazio

Validação frontend (mínima — backend é fonte da verdade):

  • Nome: não vazio
  • E-mail: formato básico
  • Senha: mínimo 8 chars se preenchida

Migration Strategy

  1. Gerar migration vazia: flask db revision --autogenerate -m "enrich_client_users_profile"
  2. Revisar script gerado — confirmar que down_revision = 'f1a2b3c4d5e6'
  3. Usar batch_alter_table para compatibilidade com SQLite (conftest de testes usa SQLite in-memory)
  4. Executar: flask db upgrade head
  5. Verificar colunas: \d client_users no psql

Atenção: O model já está sincronizado. Se autogenerate detectar diferença entre o model e o banco, ele gerará as colunas automaticamente. Caso contrário, escrever o upgrade() manualmente conforme o script acima.

Project Structure

Documentation (this feature)

.specify/features/011-enrich-client-profile/
├── plan.md              # Este arquivo
└── tasks.md             # Fase 2 (gerado por /speckit.tasks)

Source Code (repository root)

backend/
├── app/
│   ├── models/
│   │   └── user.py              # SEM ALTERAÇÃO — 12 colunas já presentes
│   ├── schemas/
│   │   └── auth.py              # ATUALIZAR — adicionar ClientUserOut, ClientUserCreateIn, ClientUserUpdateIn
│   └── routes/
│       └── admin.py             # ATUALIZAR — usar novos schemas nos endpoints de clientes
└── migrations/
    └── versions/
        └── xxxx_enrich_client_users_profile.py   # NOVO — batch_alter_table 12 colunas

frontend/
└── src/
    ├── types/
    │   └── clientUser.ts                          # NOVO — interface ClientUser expandida
    └── pages/
        └── admin/
            ├── AdminClientesPage.tsx              # REESCRITA — tabela rica + busca + avatar
            └── ClienteForm.tsx                    # REESCRITA — 5 seções + máscaras

Complexity Tracking

Item Decisão Alternativas rejeitadas
Máscaras de campo Implementação manual via onChange react-input-mask ou imask — dependência extra para funcionalidade trivial
Busca de clientes Filtragem local no frontend Endpoint GET /clientes?q=... — overkill para o volume esperado de clientes no MVP
Armazenamento de CPF Somente dígitos no banco, máscara apenas na UI Armazenar formatado — dificulta queries futuras
Validação de CPF Apenas formato (tamanho) Dígito verificador — custo/benefício baixo no MVP
Formulário full-screen Padrão de PropertyForm reutilizado Modal/slide-over — inconsistente com o padrão do admin panel