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
35
specs/027-config-pagina-contato/checklists/requirements.md
Normal file
35
specs/027-config-pagina-contato/checklists/requirements.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Specification Quality Checklist: Configuração da Página de Contato (Admin)
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-04-21
|
||||
**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 aprovada sem necessidade de clarificações — todos os campos, escopo, padrão de autenticação e comportamento de fallback foram especificados pelo solicitante.
|
||||
- Pronta para `/speckit.plan`.
|
||||
151
specs/027-config-pagina-contato/spec.md
Normal file
151
specs/027-config-pagina-contato/spec.md
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# Feature Specification: Configuração da Página de Contato (Admin)
|
||||
|
||||
**Feature Branch**: `027-config-pagina-contato`
|
||||
**Created**: 2026-04-21
|
||||
**Status**: Draft
|
||||
|
||||
---
|
||||
|
||||
## Contexto
|
||||
|
||||
A página `/contato` do site exibe informações institucionais de contato — endereço, telefone, e-mail e horário de atendimento — atualmente fixadas no código-fonte do frontend. Qualquer alteração nessas informações exige um deploy de código, o que cria dependência técnica para uma tarefa puramente operacional.
|
||||
|
||||
Esta spec cobre a criação de uma configuração persistida em banco de dados que o administrador pode editar pelo painel admin, tornando o conteúdo da página de contato dinâmico e gerenciável sem necessidade de deploy.
|
||||
|
||||
O padrão adotado é o mesmo já utilizado para a `HomepageConfig`: tabela singleton (sempre `id = 1`), endpoint público de leitura e endpoint protegido de escrita acessível apenas por administradores autenticados.
|
||||
|
||||
---
|
||||
|
||||
## User Scenarios & Testing
|
||||
|
||||
### User Story 1 — Administrador Atualiza as Informações de Contato (Priority: P1)
|
||||
|
||||
O administrador da imobiliária precisa atualizar o endereço, telefone, e-mail ou horário de atendimento sem depender de um desenvolvedor ou deploy de código.
|
||||
|
||||
**Why this priority**: É o núcleo da feature. Sem a capacidade de edição pelo admin, todo o restante não tem valor.
|
||||
|
||||
**Independent Test**: Autenticar como administrador, acessar a página de configuração de contato no painel admin (`/admin/contact-config`), alterar o campo de telefone para um novo valor e salvar. Verificar que a resposta da API pública `GET /api/v1/contact-config` retorna o novo valor e que a página `/contato` do site exibe o telefone atualizado após recarregar.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** um administrador autenticado no painel admin, **When** ele acessa `/admin/contact-config`, **Then** um formulário é exibido com os valores atuais dos campos de endereço, telefone, e-mail e horário de atendimento já preenchidos.
|
||||
2. **Given** o formulário preenchido com os valores atuais, **When** o admin altera o campo de telefone e clica em "Salvar", **Then** as alterações são persistidas e uma mensagem de sucesso é exibida na tela.
|
||||
3. **Given** que a configuração foi salva com sucesso, **When** a API pública de configuração de contato é consultada, **Then** ela retorna os novos valores imediatamente, sem necessidade de reiniciar o sistema.
|
||||
4. **Given** um administrador autenticado, **When** ele tenta salvar o formulário com o campo de e-mail em branco, **Then** o campo é destacado com erro de validação e o envio é bloqueado.
|
||||
5. **Given** um administrador autenticado, **When** ele tenta salvar com um endereço de e-mail em formato inválido, **Then** o campo é destacado com erro de validação antes do envio ser processado.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 — Página de Contato Exibe Informações Dinâmicas (Priority: P1)
|
||||
|
||||
Um visitante do site acessa a página `/contato` e vê as informações de contato mais recentes cadastradas pelo administrador, sem nenhuma interação adicional necessária.
|
||||
|
||||
**Why this priority**: É o consumidor final da configuração. Sem a integração com a API, a feature não entrega valor ao visitante nem à imobiliária.
|
||||
|
||||
**Independent Test**: Com uma configuração de contato salva via painel admin, acessar `/contato` sem autenticação e verificar que o endereço, telefone, e-mail e horário de atendimento exibidos correspondem exatamente aos valores salvos — e não aos dados anteriormente fixados no código.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** uma configuração de contato salva no sistema, **When** qualquer visitante acessa `/contato`, **Then** a página exibe o endereço (rua, bairro/cidade e CEP), telefone, e-mail e horário de atendimento provenientes da API.
|
||||
2. **Given** que o administrador atualizou o horário de atendimento, **When** um visitante recarrega `/contato`, **Then** o novo horário é exibido imediatamente.
|
||||
3. **Given** que a API de configuração de contato está indisponível, **When** um visitante acessa `/contato`, **Then** a página exibe um estado de carregamento ou uma mensagem informativa, sem exibir dados desatualizados ou causar erro de renderização crítico.
|
||||
4. **Given** um visitante não autenticado, **When** ele acessa a rota pública `GET /api/v1/contact-config` diretamente, **Then** a resposta retorna os dados de configuração sem exigir autenticação.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 — Proteção do Endpoint de Edição (Priority: P1)
|
||||
|
||||
Apenas administradores autenticados podem alterar a configuração de contato. Tentativas não autorizadas são bloqueadas.
|
||||
|
||||
**Why this priority**: Segurança é requisito não-negociável para qualquer endpoint de escrita no painel admin. O impacto de um acesso não autorizado incluiria exibição de informações falsas para todos os visitantes do site.
|
||||
|
||||
**Independent Test**: Enviar uma requisição `PUT /admin/contact-config` sem token de autenticação (ou com token de usuário comum) e verificar que a resposta é HTTP 401 ou 403. Verificar também que os dados salvos no banco não foram alterados.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** uma requisição `PUT /admin/contact-config` sem token de autenticação, **When** a requisição é processada, **Then** o sistema responde com erro de acesso não autorizado (HTTP 401) e não altera nenhum dado.
|
||||
2. **Given** uma requisição `PUT /admin/contact-config` com um token de usuário comum (não administrador), **When** a requisição é processada, **Then** o sistema responde com erro de permissão insuficiente (HTTP 403) e não altera nenhum dado.
|
||||
3. **Given** uma requisição `PUT /admin/contact-config` com token de administrador válido, **When** os dados enviados são válidos, **Then** a configuração é atualizada e o sistema retorna os dados atualizados (HTTP 200).
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- O que acontece se a tabela `contact_config` estiver vazia (nenhuma configuração foi salva ainda)? O endpoint público deve retornar valores padrão pré-populados ou um erro?
|
||||
- Como a página `/contato` se comporta durante o carregamento inicial enquanto aguarda a resposta da API?
|
||||
- O que acontece se o campo `business_hours` for enviado com um texto excessivamente longo?
|
||||
- Como o formulário admin lida com falha de rede ao tentar salvar — o usuário perde as alterações não salvas?
|
||||
- O que acontece se dois administradores tentarem salvar a configuração simultaneamente?
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
#### Grupo 1 — Dados e Persistência
|
||||
|
||||
- **FR-001**: O sistema DEVE armazenar as informações de configuração de contato em uma tabela singleton de forma que exista sempre exatamente um registro com `id = 1`, criado automaticamente na primeira leitura ou escrita caso ainda não exista.
|
||||
- **FR-002**: A configuração DEVE incluir os seguintes campos: logradouro do endereço, complemento de bairro/cidade, CEP, telefone, e-mail e horário de atendimento (texto livre multilinha).
|
||||
- **FR-003**: Todos os campos DEVEM ser obrigatórios — nenhum pode ser salvo como nulo ou vazio.
|
||||
- **FR-004**: O campo de e-mail DEVE ser validado quanto ao formato antes de ser persistido.
|
||||
- **FR-005**: O sistema DEVE registrar automaticamente a data e hora da última atualização da configuração.
|
||||
|
||||
#### Grupo 2 — API Pública de Leitura
|
||||
|
||||
- **FR-006**: O sistema DEVE disponibilizar um endpoint público de leitura de configuração de contato acessível sem autenticação.
|
||||
- **FR-007**: O endpoint público DEVE retornar todos os campos da configuração em formato estruturado (um objeto com os campos nomeados).
|
||||
- **FR-008**: O endpoint público DEVE retornar os valores padrão (correspondentes aos dados atualmente fixados no código) caso nenhuma configuração tenha sido salva ainda, em vez de retornar erro.
|
||||
|
||||
#### Grupo 3 — API Protegida de Escrita
|
||||
|
||||
- **FR-009**: O sistema DEVE disponibilizar um endpoint protegido de atualização de configuração de contato acessível apenas por administradores autenticados.
|
||||
- **FR-010**: O endpoint protegido DEVE rejeitar requisições sem token de autenticação válido com HTTP 401.
|
||||
- **FR-011**: O endpoint protegido DEVE rejeitar tokens de usuários com perfil diferente de administrador com HTTP 403.
|
||||
- **FR-012**: O endpoint protegido DEVE validar todos os campos recebidos antes de persistir e retornar erros de validação específicos por campo em caso de dados inválidos (HTTP 422).
|
||||
- **FR-013**: Após persistência bem-sucedida, o endpoint DEVE retornar os dados atualizados incluindo a data de última atualização.
|
||||
|
||||
#### Grupo 4 — Interface Admin
|
||||
|
||||
- **FR-014**: O painel admin DEVE disponibilizar uma página de edição de configuração de contato (`/admin/contact-config`) acessível apenas a administradores autenticados.
|
||||
- **FR-015**: A página admin DEVE carregar automaticamente os valores atuais da configuração ao ser aberta e pré-preencher o formulário.
|
||||
- **FR-016**: O formulário DEVE conter campos de texto para logradouro, bairro/cidade, CEP, telefone, e-mail e uma área de texto para horário de atendimento.
|
||||
- **FR-017**: O formulário DEVE exibir erros de validação inline por campo antes de tentar salvar no servidor, quando possível (ex.: e-mail com formato inválido, campo obrigatório vazio).
|
||||
- **FR-018**: O formulário DEVE exibir uma notificação de sucesso após salvar com êxito e uma notificação de erro em caso de falha na requisição ao servidor.
|
||||
- **FR-019**: O botão de salvar DEVE ser desabilitado enquanto a requisição de salvamento estiver em andamento, para evitar submissões duplicadas.
|
||||
|
||||
#### Grupo 5 — Página Pública de Contato
|
||||
|
||||
- **FR-020**: A página `/contato` DEVE buscar as informações de contato da API pública em vez de usar valores fixados no código.
|
||||
- **FR-021**: A página `/contato` DEVE exibir um indicador de carregamento enquanto aguarda a resposta da API.
|
||||
- **FR-022**: A página `/contato` DEVE continuar renderizando normalmente em caso de falha na API, exibindo uma mensagem informativa no lugar das informações de contato.
|
||||
- **FR-023**: A estrutura visual e o layout da página `/contato` NÃO DEVEM ser alterados por esta feature — apenas a origem dos dados muda.
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **ContactConfig**: Registro singleton de configuração de contato da imobiliária. Atributos: logradouro, bairro/cidade, CEP, telefone, e-mail, horário de atendimento (texto multilinha), data de última atualização. Relacionamentos: nenhum — é uma entidade independente de configuração.
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: O administrador consegue atualizar qualquer campo de contato em menos de 1 minuto, do acesso à página admin até a confirmação de salvamento.
|
||||
- **SC-002**: A página `/contato` reflete as alterações feitas pelo admin imediatamente após o recarregamento da página, sem necessidade de nenhuma intervenção técnica.
|
||||
- **SC-003**: 100% das tentativas de acesso não autenticado ao endpoint de escrita são bloqueadas com resposta de erro apropriada.
|
||||
- **SC-004**: A página `/contato` permanece funcional e renderizável mesmo quando a API de configuração retorna erro, sem quebrar a experiência do visitante.
|
||||
- **SC-005**: Nenhum campo de configuração de contato permanece fixado no código-fonte do frontend após a conclusão da feature.
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
- O padrão singleton (`get_or_create` com `id = 1`) já é conhecido e usado na codebase (`HomepageConfig`); esta feature segue exatamente o mesmo padrão.
|
||||
- Os valores padrão (usados como fallback quando nenhuma configuração existe) são os atualmente hardcoded na página `/contato`: endereço "Rua das Imobiliárias, 123 / Centro — São Paulo, SP / CEP 01000-000", telefone "(11) 99999-0000", e-mail "contato@imobiliariahub.com.br" e horário conforme texto atual.
|
||||
- O mecanismo de autenticação e autorização de admin (`require_admin`) já existe e será reutilizado sem modificações.
|
||||
- Não há necessidade de histórico de versões da configuração — apenas o valor atual importa.
|
||||
- O campo `business_hours` é texto livre; a formatação de exibição (ex.: quebras de linha) é responsabilidade do componente de apresentação, não da API.
|
||||
- Esta feature não altera o design, layout ou demais seções da página `/contato` — apenas substitui os dados hardcoded por dados dinâmicos.
|
||||
- A migração de banco de dados para criar a tabela `contact_config` será gerada via Alembic, seguindo o padrão já adotado no projeto.
|
||||
- A seed inicial que popula a tabela com os valores padrão é opcional — o comportamento de fallback no endpoint público é suficiente para o primeiro acesso.
|
||||
404
specs/027-config-pagina-contato/tasks.md
Normal file
404
specs/027-config-pagina-contato/tasks.md
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
# Tasks: Feature 027 — Configuração da Página de Contato (Admin)
|
||||
|
||||
**Branch**: `027-config-pagina-contato`
|
||||
**Spec**: `specs/027-config-pagina-contato/spec.md`
|
||||
**Última migration**: `g1h2i3j4k5l6_add_source_to_contact_leads.py`
|
||||
|
||||
---
|
||||
|
||||
## Fase 1 — Foundational: Backend Core (Pré-requisito para todos os user stories)
|
||||
|
||||
> **Objetivo**: Criar a tabela `contact_config`, o modelo ORM, os schemas Pydantic e o
|
||||
> endpoint público de leitura. Nenhum user story pode ser implementado antes desta fase.
|
||||
>
|
||||
> **⚠️ CRÍTICO**: Concluir inteiramente antes de iniciar as fases 2 e 3.
|
||||
|
||||
- [ ] T001 Gerar migration Alembic para criar tabela `contact_config` com INSERT inicial em `backend/migrations/versions/h2i3j4k5l6m7_add_contact_config.py`
|
||||
|
||||
**Comando para gerar a migration** (executar de dentro do container ou com `.venv`):
|
||||
```bash
|
||||
flask --app run:app db revision --autogenerate -m "add_contact_config"
|
||||
```
|
||||
|
||||
A migration deve:
|
||||
1. Criar a tabela com os campos abaixo:
|
||||
```python
|
||||
op.create_table(
|
||||
"contact_config",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("address_street", sa.String(200), nullable=False),
|
||||
sa.Column("address_neighborhood_city", sa.String(200), nullable=False),
|
||||
sa.Column("address_zip", sa.String(20), nullable=False),
|
||||
sa.Column("phone", sa.String(30), nullable=False),
|
||||
sa.Column("email", sa.String(254), nullable=False),
|
||||
sa.Column("business_hours", sa.Text, nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime, nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
)
|
||||
```
|
||||
2. Inserir o registro singleton com os valores atualmente hardcoded:
|
||||
```python
|
||||
op.execute("""
|
||||
INSERT INTO contact_config
|
||||
(id, address_street, address_neighborhood_city, address_zip,
|
||||
phone, email, business_hours, updated_at)
|
||||
VALUES
|
||||
(1, 'Rua das Imobiliárias, 123',
|
||||
'Centro — São Paulo, SP',
|
||||
'CEP 01000-000',
|
||||
'(11) 99999-0000',
|
||||
'contato@imobiliariahub.com.br',
|
||||
'Segunda a Sexta: 9h às 18h\nSábado: 9h às 13h',
|
||||
NOW())
|
||||
""")
|
||||
```
|
||||
|
||||
- [ ] T002 Criar modelo `ContactConfig` em `backend/app/models/contact_config.py` seguindo o padrão de `HomepageConfig`
|
||||
|
||||
```python
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class ContactConfig(db.Model):
|
||||
__tablename__ = "contact_config"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
address_street = db.Column(db.String(200), nullable=False)
|
||||
address_neighborhood_city = db.Column(db.String(200), nullable=False)
|
||||
address_zip = db.Column(db.String(20), nullable=False)
|
||||
phone = db.Column(db.String(30), nullable=False)
|
||||
email = db.Column(db.String(254), nullable=False)
|
||||
business_hours = db.Column(db.Text, nullable=False)
|
||||
updated_at = db.Column(
|
||||
db.DateTime,
|
||||
nullable=False,
|
||||
server_default=db.func.now(),
|
||||
onupdate=db.func.now(),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ContactConfig id={self.id!r}>"
|
||||
```
|
||||
|
||||
- [ ] T003 [P] Criar schemas Pydantic em `backend/app/schemas/contact_config.py` seguindo o padrão de `HomepageConfigOut`/`HomepageConfigIn`
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, field_validator
|
||||
|
||||
|
||||
class ContactConfigOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
address_street: str
|
||||
address_neighborhood_city: str
|
||||
address_zip: str
|
||||
phone: str
|
||||
email: str
|
||||
business_hours: str
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class ContactConfigIn(BaseModel):
|
||||
address_street: str
|
||||
address_neighborhood_city: str
|
||||
address_zip: str
|
||||
phone: str
|
||||
email: EmailStr
|
||||
business_hours: str
|
||||
|
||||
@field_validator("address_street", "address_neighborhood_city", "address_zip",
|
||||
"phone", "business_hours")
|
||||
@classmethod
|
||||
def not_empty(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("Campo não pode ser vazio")
|
||||
return v
|
||||
```
|
||||
|
||||
- [ ] T004 Criar rota pública `GET /api/v1/contact-config` em `backend/app/routes/contact_config.py` e registrar o blueprint em `backend/app/__init__.py`
|
||||
|
||||
**`backend/app/routes/contact_config.py`**:
|
||||
```python
|
||||
from flask import Blueprint, jsonify
|
||||
|
||||
from app.models.contact_config import ContactConfig
|
||||
from app.schemas.contact_config import ContactConfigOut
|
||||
|
||||
contact_config_bp = Blueprint("contact_config", __name__, url_prefix="/api/v1")
|
||||
|
||||
|
||||
@contact_config_bp.get("/contact-config")
|
||||
def get_contact_config():
|
||||
config = ContactConfig.query.first()
|
||||
if config is None:
|
||||
return jsonify({"error": "Contact config not found"}), 404
|
||||
return jsonify(ContactConfigOut.model_validate(config).model_dump(mode="json"))
|
||||
```
|
||||
|
||||
**`backend/app/__init__.py`** — adicionar após o import de `homepage_bp`:
|
||||
```python
|
||||
from app.routes.contact_config import contact_config_bp
|
||||
```
|
||||
E registrar junto aos demais blueprints:
|
||||
```python
|
||||
app.register_blueprint(contact_config_bp)
|
||||
```
|
||||
|
||||
**Checkpoint — Fase 1 concluída**: `GET /api/v1/contact-config` retorna os dados do banco. As fases 2 e 3 podem ser iniciadas em paralelo.
|
||||
|
||||
---
|
||||
|
||||
## Fase 2 — User Stories 1 + 3: Admin Edita Configuração e Endpoint Protegido (P1)
|
||||
|
||||
> **Objetivo**: Administrador acessa `/admin/contact-config`, vê o formulário preenchido
|
||||
> com os valores atuais, edita e salva. O endpoint PUT rejeita acessos não autorizados.
|
||||
>
|
||||
> **Teste independente**: Autenticar como admin, acessar `/admin/contact-config`,
|
||||
> alterar o telefone, clicar em "Salvar". Verificar `GET /api/v1/contact-config` retorna o
|
||||
> novo valor. Verificar que `PUT /api/v1/admin/contact-config` sem token retorna HTTP 401.
|
||||
|
||||
### Implementação — User Stories 1 + 3
|
||||
|
||||
- [ ] T005 [US1] Adicionar rota `PUT /api/v1/admin/contact-config` em `backend/app/routes/admin.py` com `@require_admin`
|
||||
|
||||
**Adicionar imports** no topo de `backend/app/routes/admin.py`:
|
||||
```python
|
||||
from app.models.contact_config import ContactConfig
|
||||
from app.schemas.contact_config import ContactConfigIn, ContactConfigOut
|
||||
```
|
||||
|
||||
**Adicionar a rota** (em qualquer ponto lógico do arquivo, ex.: próximo a outras rotas de configuração):
|
||||
```python
|
||||
@admin_bp.put("/contact-config")
|
||||
@require_admin
|
||||
def update_contact_config():
|
||||
try:
|
||||
data = ContactConfigIn.model_validate(request.get_json(force=True) or {})
|
||||
except ValidationError as exc:
|
||||
return jsonify({"errors": exc.errors()}), 422
|
||||
|
||||
config = ContactConfig.query.first()
|
||||
if config is None:
|
||||
config = ContactConfig(id=1, **data.model_dump())
|
||||
db.session.add(config)
|
||||
else:
|
||||
for field, value in data.model_dump().items():
|
||||
setattr(config, field, value)
|
||||
|
||||
db.session.commit()
|
||||
db.session.refresh(config)
|
||||
return jsonify(ContactConfigOut.model_validate(config).model_dump(mode="json"))
|
||||
```
|
||||
|
||||
> `@require_admin` garante HTTP 401 para não-autenticados e HTTP 403 para não-admins (US3).
|
||||
|
||||
- [ ] T006 [P] [US1] Criar `frontend/src/services/contactConfig.ts` com `getContactConfig()` e `updateContactConfig()`
|
||||
|
||||
```typescript
|
||||
import { api } from './api'
|
||||
|
||||
export interface ContactConfig {
|
||||
address_street: string
|
||||
address_neighborhood_city: string
|
||||
address_zip: string
|
||||
phone: string
|
||||
email: string
|
||||
business_hours: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface ContactConfigInput {
|
||||
address_street: string
|
||||
address_neighborhood_city: string
|
||||
address_zip: string
|
||||
phone: string
|
||||
email: string
|
||||
business_hours: string
|
||||
}
|
||||
|
||||
export async function getContactConfig(): Promise<ContactConfig> {
|
||||
const response = await api.get<ContactConfig>('/contact-config')
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function updateContactConfig(data: ContactConfigInput): Promise<ContactConfig> {
|
||||
const response = await api.put<ContactConfig>('/admin/contact-config', data)
|
||||
return response.data
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] T007 [US1] Criar `frontend/src/pages/admin/AdminContactConfigPage.tsx` seguindo o padrão visual das demais páginas admin (ex.: `AdminAgentsPage.tsx`)
|
||||
|
||||
**Comportamento esperado**:
|
||||
- `useEffect` faz `GET /api/v1/contact-config` ao montar e pré-preenche o form
|
||||
- Estado local `form` com os 6 campos editáveis
|
||||
- `handleSubmit` chama `updateContactConfig(form)`, exibe toast de sucesso ou erro
|
||||
- Botão "Salvar" desabilitado enquanto `saving === true` (FR-019)
|
||||
- Validação frontend: e-mail com formato válido, campos não vazios antes de submeter (FR-017)
|
||||
- Erros de campo exibidos inline; toast global para erros de rede
|
||||
|
||||
**Estrutura do componente**:
|
||||
```tsx
|
||||
import { useState, useEffect } from 'react'
|
||||
import { getContactConfig, updateContactConfig } from '../../services/contactConfig'
|
||||
import type { ContactConfigInput } from '../../services/contactConfig'
|
||||
|
||||
const emptyForm: ContactConfigInput = {
|
||||
address_street: '',
|
||||
address_neighborhood_city: '',
|
||||
address_zip: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
business_hours: '',
|
||||
}
|
||||
|
||||
export default function AdminContactConfigPage() {
|
||||
const [form, setForm] = useState<ContactConfigInput>(emptyForm)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
getContactConfig()
|
||||
.then(data => {
|
||||
const { updated_at, ...editable } = data
|
||||
setForm(editable)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setSaving(true)
|
||||
setError(null)
|
||||
setSuccess(false)
|
||||
try {
|
||||
await updateContactConfig(form)
|
||||
setSuccess(true)
|
||||
} catch {
|
||||
setError('Erro ao salvar. Tente novamente.')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Renderizar: loading skeleton → formulário com 5 inputs + 1 textarea + botão salvar
|
||||
// Campos: Logradouro, Bairro/Cidade, CEP, Telefone, E-mail, Horário de Atendimento
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] T008 [US1] Registrar rota `/admin/contact-config` em `frontend/src/App.tsx` e adicionar item `{ to: '/admin/contact-config', label: 'Conf. Contato' }` em `adminNavItems` em `frontend/src/components/Navbar.tsx`
|
||||
|
||||
**`frontend/src/App.tsx`** — localizar o trecho de rotas admin e adicionar:
|
||||
```tsx
|
||||
import AdminContactConfigPage from './pages/admin/AdminContactConfigPage'
|
||||
// ...
|
||||
<Route path="/admin/contact-config" element={<AdminContactConfigPage />} />
|
||||
```
|
||||
|
||||
**`frontend/src/components/Navbar.tsx`** — acrescentar ao array `adminNavItems`:
|
||||
```typescript
|
||||
{ to: '/admin/contact-config', label: 'Conf. Contato' },
|
||||
```
|
||||
|
||||
**Checkpoint — Fase 2 concluída**: Admin consegue editar e salvar a configuração de contato. Endpoint PUT retorna 401/403 para acessos não autorizados.
|
||||
|
||||
---
|
||||
|
||||
## Fase 3 — User Story 2: Página Pública de Contato Exibe Dados Dinâmicos (P1)
|
||||
|
||||
> **Objetivo**: A página `/contato` deixa de usar dados hardcoded e passa a consumir
|
||||
> `GET /api/v1/contact-config`, preservando layout e estrutura visual existentes.
|
||||
>
|
||||
> **Teste independente**: Sem autenticação, acessar `/contato` e verificar que os dados
|
||||
> exibidos correspondem ao banco (alterado via painel admin). A estrutura visual não muda.
|
||||
|
||||
### Implementação — User Story 2
|
||||
|
||||
- [ ] T009 [US2] Atualizar `frontend/src/pages/ContactPage.tsx` para consumir `getContactConfig()` no lugar dos dados hardcoded
|
||||
|
||||
**Comportamento esperado**:
|
||||
- `useEffect` chama `getContactConfig()` ao montar
|
||||
- Estado `config` inicializado como `null`; enquanto `loading === true` exibir skeleton ou spinner no lugar dos dados de contato (FR-021)
|
||||
- Em caso de erro na requisição, exibir mensagem informativa em lugar dos dados — não renderizar valores obsoletos nem lançar erro de renderização (FR-022)
|
||||
- Layout, classes CSS e demais seções da página NÃO devem ser alterados (FR-023)
|
||||
|
||||
**Campos a substituir** (remover literais hardcoded e usar `config.campo`):
|
||||
- Endereço: `config.address_street`, `config.address_neighborhood_city`, `config.address_zip`
|
||||
- Telefone: `config.phone`
|
||||
- E-mail: `config.email`
|
||||
- Horário de atendimento: `config.business_hours` (renderizar com `white-space: pre-line` ou equivalente para preservar quebras de linha)
|
||||
|
||||
**Exemplo de estrutura**:
|
||||
```tsx
|
||||
import { useState, useEffect } from 'react'
|
||||
import { getContactConfig } from '../services/contactConfig'
|
||||
import type { ContactConfig } from '../services/contactConfig'
|
||||
|
||||
export default function ContactPage() {
|
||||
const [config, setConfig] = useState<ContactConfig | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [fetchError, setFetchError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
getContactConfig()
|
||||
.then(setConfig)
|
||||
.catch(() => setFetchError(true))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
// ...restante do JSX existente — apenas substituir as strings hardcoded
|
||||
// por {loading ? <Skeleton /> : fetchError ? <p>Informações indisponíveis</p> : config?.campo}
|
||||
}
|
||||
```
|
||||
|
||||
**Checkpoint — Fase 3 concluída**: `/contato` exibe dados dinâmicos da API. Todos os user stories são funcionais e testáveis de forma independente.
|
||||
|
||||
---
|
||||
|
||||
## Fase 4 — Polish & Verificações Finais
|
||||
|
||||
- [ ] T010 [P] Verificar que `backend/app/models/__init__.py` exporta `ContactConfig` (se o arquivo contiver imports explícitos dos modelos)
|
||||
|
||||
Se o arquivo importar modelos explicitamente, adicionar:
|
||||
```python
|
||||
from app.models.contact_config import ContactConfig # noqa: F401
|
||||
```
|
||||
|
||||
- [ ] T011 [P] Aplicar a migration no banco de dados e verificar o registro singleton
|
||||
|
||||
```bash
|
||||
# Dentro do container ou com .venv ativo:
|
||||
flask --app run:app db upgrade
|
||||
# Verificar:
|
||||
# SELECT * FROM contact_config; → deve retornar 1 linha com id=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependências entre Tasks
|
||||
|
||||
```
|
||||
T001 → T002 → T003 → T004 (blueprint público)
|
||||
↓
|
||||
T005 (PUT admin)
|
||||
↓
|
||||
T006 → T007 → T008 (rotas frontend)
|
||||
T006 → T009 (ContactPage)
|
||||
```
|
||||
|
||||
**Execução paralela possível**:
|
||||
- T003 pode começar em paralelo com T002 (schemas não importam o modelo diretamente)
|
||||
- T006, T007, T008, T009 podem ser desenvolvidos em paralelo após T001–T004
|
||||
|
||||
---
|
||||
|
||||
## Escopo MVP
|
||||
|
||||
O **MVP mínimo** é completar as fases 1, 2 e 3 integralmente — as três user stories têm
|
||||
prioridade P1 e são interdependentes para entregar valor. A fase 4 é verificação final.
|
||||
Loading…
Add table
Add a link
Reference in a new issue