# Implementation Plan: Enriquecimento do Perfil de Cliente **Branch**: `011-enrich-client-profile` | **Date**: 2026-04-14 | **Spec**: [spec.md](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/ — 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**: ```python 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` ```python 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` ```python 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` ```python 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 `UserOut` → `ClientUserOut` na serialização | | `POST /api/v1/admin/clientes` | Criar | Validar com `ClientUserCreateIn`; persistir os 12 novos campos | | `PUT /api/v1/admin/clientes/` | Editar | Validar com `ClientUserUpdateIn`; atualizar campos presentes (ignorar `None`) | **Padrão de update** (apenas campos enviados): ```python 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` ```typescript 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`) - `useEffect` → `GET /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) ```text .specify/features/011-enrich-client-profile/ ├── plan.md # Este arquivo └── tasks.md # Fase 2 (gerado por /speckit.tasks) ``` ### Source Code (repository root) ```text 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 |