fix(frontend): align auth docs and ignore generated files
This commit is contained in:
parent
b52f780e26
commit
fdea7495c0
11 changed files with 470 additions and 1057 deletions
6
frontend/.gitignore
vendored
6
frontend/.gitignore
vendored
|
|
@ -8,16 +8,12 @@ dist/
|
||||||
.dist/
|
.dist/
|
||||||
dist-ssr/
|
dist-ssr/
|
||||||
build/
|
build/
|
||||||
*.local
|
.next/
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
.env*
|
.env*
|
||||||
!.env.example
|
!.env.example
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs/
|
logs/
|
||||||
|
|
|
||||||
1
frontend/.next/cache/eslint/.cache_1lfc3o4
vendored
1
frontend/.next/cache/eslint/.cache_1lfc3o4
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,164 +1,49 @@
|
||||||
# Implementação do Dashboard com Dados Reais
|
# Implementação do Dashboard com Dados Reais
|
||||||
|
|
||||||
## Resumo da Implementação
|
## Nota de Atualização
|
||||||
|
|
||||||
Implementei um sistema completo de dashboard que exibe dados reais do banco de dados para todos os níveis de usuários (SUPERADMIN, ADMIN, COLABORADOR), respeitando as regras de negócio de cada nível.
|
Este documento continua útil para explicar a lógica do dashboard, mas a taxonomia antiga de papéis mudou.
|
||||||
|
|
||||||
## Arquivos Criados/Modificados
|
Papéis válidos hoje no fluxo ativo:
|
||||||
|
- `admin`
|
||||||
|
- `owner`
|
||||||
|
- `employee`
|
||||||
|
- `delivery`
|
||||||
|
|
||||||
### 1. Dashboard Service (`src/services/dashboardService.ts`)
|
`superadmin` não faz mais parte do modelo ativo.
|
||||||
|
|
||||||
- **Objetivo**: Centralizar a lógica de busca e processamento de dados do dashboard
|
## Resumo
|
||||||
- **Funcionalidades**:
|
|
||||||
- Obter estatísticas baseadas no nível do usuário
|
|
||||||
- Calcular métricas em tempo real (medicamentos disponíveis, próximos ao vencimento, pedidos, vendas, economia)
|
|
||||||
- Filtrar dados por empresa quando necessário
|
|
||||||
- Buscar atividades recentes
|
|
||||||
- Listar pedidos pendentes
|
|
||||||
|
|
||||||
### 2. API Route (`src/app/api/dashboard/route.ts`)
|
O dashboard consome dados reais e aplica regras de visibilidade por perfil. A implementação foi pensada para separar:
|
||||||
|
- visão global administrativa
|
||||||
|
- visão da empresa
|
||||||
|
- visão operacional limitada
|
||||||
|
|
||||||
- **Objetivo**: Endpoint para servir dados do dashboard
|
## Regras de negócio vigentes
|
||||||
- **Endpoints**:
|
|
||||||
- `GET /api/dashboard?userRole=X&empresaId=Y&type=stats` - Estatísticas
|
|
||||||
- `GET /api/dashboard?userRole=X&empresaId=Y&type=activities` - Atividades recentes
|
|
||||||
- `GET /api/dashboard?userRole=X&empresaId=Y&type=pending` - Pedidos pendentes
|
|
||||||
|
|
||||||
### 3. Hook Customizado (`src/hooks/useDashboardData.ts`)
|
|
||||||
|
|
||||||
- **Objetivo**: Hook React para consumir dados do dashboard
|
|
||||||
- **Funcionalidades**:
|
|
||||||
- Estados de loading, erro e dados
|
|
||||||
- Atualização automática baseada em mudanças de role/empresa
|
|
||||||
- Tratamento de erros
|
|
||||||
|
|
||||||
### 4. Dashboard Principal Atualizado (`src/app/dashboard/page.tsx`)
|
|
||||||
|
|
||||||
- **Modificações**:
|
|
||||||
- Integração com dados reais via hook
|
|
||||||
- Exibição condicional baseada no nível do usuário
|
|
||||||
- Indicadores de carregamento e erro
|
|
||||||
- Substituição de dados mockados por dados reais
|
|
||||||
|
|
||||||
## Regras de Negócio Implementadas
|
|
||||||
|
|
||||||
### SUPERADMIN
|
|
||||||
|
|
||||||
- **Visualiza**: Dados globais de todas as empresas
|
|
||||||
- **Métricas**:
|
|
||||||
- Total de medicamentos disponíveis (todas as empresas)
|
|
||||||
- Medicamentos próximos ao vencimento (próximos 30 dias)
|
|
||||||
- Todos os pedidos recebidos no sistema
|
|
||||||
- Pedidos pendentes globais
|
|
||||||
- Vendas do mês (todas as empresas)
|
|
||||||
- Economia gerada total
|
|
||||||
- **Atividades**: Recentes de todo o sistema
|
|
||||||
- **Pedidos Pendentes**: De todas as empresas
|
|
||||||
|
|
||||||
### ADMIN
|
### ADMIN
|
||||||
|
|
||||||
- **Visualiza**: Dados apenas da sua empresa
|
- Visualiza dados globais e administrativos do sistema
|
||||||
- **Métricas**:
|
- Pode acessar métricas agregadas
|
||||||
- Medicamentos disponíveis da empresa
|
- Pode acessar pedidos pendentes e atividades recentes globais
|
||||||
- Medicamentos próximos ao vencimento da empresa
|
|
||||||
- Pedidos recebidos onde a empresa é vendedora
|
|
||||||
- Pedidos pendentes da empresa
|
|
||||||
- Vendas do mês da empresa
|
|
||||||
- Economia gerada pela empresa
|
|
||||||
- **Atividades**: Recentes da empresa
|
|
||||||
- **Pedidos Pendentes**: Apenas da empresa
|
|
||||||
|
|
||||||
### COLABORADOR
|
### OWNER
|
||||||
|
|
||||||
- **Visualiza**: Dados limitados da empresa
|
- Visualiza dados apenas da própria empresa
|
||||||
- **Métricas**:
|
- Pode acessar métricas da empresa, pedidos recebidos e dados financeiros da própria operação
|
||||||
- Medicamentos disponíveis da empresa
|
|
||||||
- Medicamentos próximos ao vencimento da empresa
|
|
||||||
- **NÃO visualiza**: Pedidos, vendas, economia (dados sensíveis)
|
|
||||||
- **Atividades**: Limitadas ou não exibidas
|
|
||||||
- **Pedidos Pendentes**: Não visualiza
|
|
||||||
|
|
||||||
## Funcionalidades Implementadas
|
### EMPLOYEE
|
||||||
|
|
||||||
### 1. Métricas Dinâmicas
|
- Visualiza dados limitados da empresa
|
||||||
|
- Não deve acessar métricas financeiras sensíveis
|
||||||
|
- Não deve acessar visão administrativa global
|
||||||
|
|
||||||
- ✅ Contagem real de medicamentos por empresa
|
### DELIVERY
|
||||||
- ✅ Cálculo de medicamentos próximos ao vencimento (30 dias)
|
|
||||||
- ✅ Contagem de pedidos recebidos e pendentes
|
|
||||||
- ✅ Cálculo de vendas do mês atual
|
|
||||||
- ✅ Soma da economia gerada (valor total dos pedidos concluídos)
|
|
||||||
|
|
||||||
### 2. Atividades Recentes
|
- Atua em fluxos operacionais de entrega
|
||||||
|
- Não compartilha o mesmo escopo analítico de `admin` e `owner`
|
||||||
|
|
||||||
- ✅ Lista de pedidos recentes com detalhes
|
## Observações
|
||||||
- ✅ Lista de produtos atualizados recentemente
|
|
||||||
- ✅ Formatação de datas e valores em português
|
|
||||||
- ✅ Status coloridos para diferentes tipos de atividade
|
|
||||||
|
|
||||||
### 3. Pedidos Pendentes
|
- Trechos antigos que mencionam `SUPERADMIN`, `ADMIN` e `COLABORADOR` devem ser lidos como nomenclatura histórica
|
||||||
|
- No fluxo atual, use sempre os nomes canônicos: `admin`, `owner`, `employee`, `delivery`
|
||||||
- ✅ Lista de pedidos com status pendente ou aguardando aprovação
|
|
||||||
- ✅ Informações: número, valor, comprador, data
|
|
||||||
- ✅ Filtrado por empresa para admins
|
|
||||||
|
|
||||||
### 4. Interface Responsiva
|
|
||||||
|
|
||||||
- ✅ Cards de métricas adaptáveis
|
|
||||||
- ✅ Indicadores de carregamento
|
|
||||||
- ✅ Mensagens de erro
|
|
||||||
- ✅ Layout responsivo para diferentes tamanhos de tela
|
|
||||||
|
|
||||||
## Como Funciona
|
|
||||||
|
|
||||||
1. **Autenticação**: O usuário faz login e o sistema identifica seu nível e empresa
|
|
||||||
2. **Carregamento**: O hook `useDashboardData` busca dados baseado no nível do usuário
|
|
||||||
3. **API**: A rota `/api/dashboard` processa a requisição e retorna dados filtrados
|
|
||||||
4. **Service**: O `dashboardService` executa queries específicas nos serviços de produto e pedido
|
|
||||||
5. **Exibição**: O componente Dashboard renderiza os dados de acordo com as permissões
|
|
||||||
|
|
||||||
## Dados Reais vs Mockados
|
|
||||||
|
|
||||||
### ANTES (Mockado)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const stats = {
|
|
||||||
medicamentosDisponiveis: 1247,
|
|
||||||
pedidosRecebidos: 28,
|
|
||||||
vendasMes: 156,
|
|
||||||
economiaGerada: 45280,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### DEPOIS (Dados Reais)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { stats, atividades, pedidosPendentes, loading, error } =
|
|
||||||
useDashboardData({
|
|
||||||
userRole: userRole || "",
|
|
||||||
empresaId: empresaId || undefined,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Os dados agora são:
|
|
||||||
|
|
||||||
- ✅ Calculados em tempo real
|
|
||||||
- ✅ Filtrados por empresa quando apropriado
|
|
||||||
- ✅ Baseados em dados reais do banco Appwrite
|
|
||||||
- ✅ Atualizados automaticamente
|
|
||||||
|
|
||||||
## Benefícios da Implementação
|
|
||||||
|
|
||||||
1. **Dados Confiáveis**: Informações sempre atualizadas do banco
|
|
||||||
2. **Segurança**: Filtros por nível de usuário e empresa
|
|
||||||
3. **Performance**: Queries otimizadas e cache no frontend
|
|
||||||
4. **Experiência**: Loading states e tratamento de erros
|
|
||||||
5. **Escalabilidade**: Fácil adição de novas métricas
|
|
||||||
6. **Manutenibilidade**: Código organizado e bem documentado
|
|
||||||
|
|
||||||
## Próximos Passos Sugeridos
|
|
||||||
|
|
||||||
1. **Cache**: Implementar cache Redis para métricas
|
|
||||||
2. **Real-time**: WebSockets para atualizações em tempo real
|
|
||||||
3. **Gráficos**: Adicionar visualizações gráficas
|
|
||||||
4. **Filtros**: Filtros por período de tempo
|
|
||||||
5. **Exportação**: Relatórios em PDF/Excel
|
|
||||||
|
|
@ -1,271 +1,32 @@
|
||||||
# Implementação Mercado Pago - Checkout SaveInMed
|
# Implementação Mercado Pago - Checkout SaveInMed
|
||||||
|
|
||||||
## Visão Geral
|
## Nota de Atualização
|
||||||
|
|
||||||
Implementação completa da integração com Mercado Pago na aplicação SaveInMed, seguindo a estrutura da API BFF existente e o curl fornecido.
|
Este documento foi escrito para uma arquitetura antiga baseada em BFF.
|
||||||
|
|
||||||
## Arquivos Implementados
|
Estado atual esperado:
|
||||||
|
- o fluxo ativo não deve depender de BFF
|
||||||
|
- referências a `bff.saveinmed.com.br` ou `bff-dev.saveinmed.com.br` são históricas
|
||||||
|
- novas integrações devem apontar para a API principal
|
||||||
|
|
||||||
### 1. Serviço Mercado Pago (`src/services/mercadoPagoService.ts`)
|
## Resumo
|
||||||
|
|
||||||
**Responsabilidades:**
|
A integração com Mercado Pago cobre:
|
||||||
- Criar preferências de pagamento via BFF
|
- criação de preferência de pagamento
|
||||||
- Verificar status de pagamentos
|
- verificação de status
|
||||||
- Processar notificações de webhook
|
- retorno de sucesso/erro
|
||||||
- Validar dados antes do envio
|
- webhook de atualização de estado
|
||||||
- Formatar valores e dados
|
|
||||||
|
|
||||||
**Principais Métodos:**
|
## Diretriz atual de infraestrutura
|
||||||
```typescript
|
|
||||||
// Criar preferência de pagamento
|
|
||||||
criarPreferencia(pedidoId, itens, dadosComprador)
|
|
||||||
|
|
||||||
// Verificar status do pagamento
|
Use backend/API principal em vez de BFF para:
|
||||||
verificarStatusPagamento(paymentId)
|
- criar preferência de pagamento
|
||||||
|
- consultar pagamento
|
||||||
|
- receber webhook
|
||||||
|
|
||||||
// Processar notificação webhook
|
## Importante
|
||||||
processarNotificacao(notificationData)
|
|
||||||
|
|
||||||
// Validar dados da preferência
|
Se este documento for usado para retomada da implementação:
|
||||||
validarDadosPreferencia(pedidoId, itens)
|
1. substituir base URLs de BFF pela API principal
|
||||||
```
|
2. validar contratos reais dos endpoints antes de codificar
|
||||||
|
3. não reintroduzir dependência em BFF no frontend ativo
|
||||||
**Endpoint BFF Utilizado:**
|
|
||||||
- `POST https://bff-dev.saveinmed.com.br/mercadopago/preference`
|
|
||||||
- `GET https://bff-dev.saveinmed.com.br/mercadopago/payment/{id}`
|
|
||||||
- `POST https://bff-dev.saveinmed.com.br/mercadopago/webhook`
|
|
||||||
|
|
||||||
### 2. Componente Botão Mercado Pago (`src/components/MercadoPagoButton.tsx`)
|
|
||||||
|
|
||||||
**Funcionalidades:**
|
|
||||||
- Botão estilizado para pagamento via Mercado Pago
|
|
||||||
- Validação automática dos dados
|
|
||||||
- Estados de loading e erro
|
|
||||||
- Redirecionamento automático para o checkout do Mercado Pago
|
|
||||||
- Callbacks para sucesso e erro
|
|
||||||
|
|
||||||
**Props:**
|
|
||||||
```typescript
|
|
||||||
interface MercadoPagoButtonProps {
|
|
||||||
pedidoId: string
|
|
||||||
itens: Array<{produto, quantidade}>
|
|
||||||
dadosComprador?: {nome, email}
|
|
||||||
onSuccess?: (preference_id, init_point) => void
|
|
||||||
onError?: (error) => void
|
|
||||||
disabled?: boolean
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Páginas de Retorno
|
|
||||||
|
|
||||||
#### Página de Sucesso (`src/app/pagamento/sucesso/page.tsx`)
|
|
||||||
- Processa retorno de pagamento aprovado
|
|
||||||
- Exibe detalhes do pagamento e pedido
|
|
||||||
- Atualiza status do pedido
|
|
||||||
- Interface amigável com próximos passos
|
|
||||||
|
|
||||||
#### Página de Erro (`src/app/pagamento/erro/page.tsx`)
|
|
||||||
- Trata erros de pagamento
|
|
||||||
- Explica motivos comuns de falha
|
|
||||||
- Oferece sugestões e alternativas
|
|
||||||
- Botões para tentar novamente ou escolher outro método
|
|
||||||
|
|
||||||
### 4. Integração no Checkout (`src/app/checkout/page.tsx`)
|
|
||||||
|
|
||||||
**Modificações Realizadas:**
|
|
||||||
- Adicionada opção "Mercado Pago" na seleção de pagamento
|
|
||||||
- Lógica específica para processar pagamento via Mercado Pago
|
|
||||||
- Botão personalizado na etapa de confirmação
|
|
||||||
- Fluxo diferenciado para redirecionamento externo
|
|
||||||
|
|
||||||
## Estrutura de Dados
|
|
||||||
|
|
||||||
### Preferência Mercado Pago
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"back_urls": {
|
|
||||||
"failure": "https://saveinmed.com.br/pagamento/erro",
|
|
||||||
"success": "https://saveinmed.com.br/pagamento/sucesso"
|
|
||||||
},
|
|
||||||
"external_reference": "pedido-123",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"currency_id": "BRL",
|
|
||||||
"id": "item-123",
|
|
||||||
"quantity": 1,
|
|
||||||
"title": "Nome do Produto",
|
|
||||||
"unit_price": 99.90
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"notification_url": "https://bff.saveinmed.com.br/mercadopago/webhook",
|
|
||||||
"payer": {
|
|
||||||
"email": "cliente@example.com",
|
|
||||||
"name": "Cliente",
|
|
||||||
"surname": "Teste"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resposta da API
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {...},
|
|
||||||
"preference_id": "123456789-abc123",
|
|
||||||
"init_point": "https://www.mercadopago.com.br/checkout/v1/redirect?pref_id=123456789-abc123",
|
|
||||||
"sandbox_init_point": "https://sandbox.mercadopago.com.br/checkout/v1/redirect?pref_id=123456789-abc123"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fluxo de Pagamento
|
|
||||||
|
|
||||||
### 1. Usuário Seleciona Mercado Pago
|
|
||||||
- Na etapa 2 do checkout, seleciona "Mercado Pago"
|
|
||||||
- Sistema avança para etapa 3 (confirmação)
|
|
||||||
|
|
||||||
### 2. Confirmação e Redirecionamento
|
|
||||||
- Usuário confirma dados na etapa 3
|
|
||||||
- Clica no botão "Pagar via Mercado Pago"
|
|
||||||
- Sistema cria preferência via BFF
|
|
||||||
- Redireciona para checkout do Mercado Pago
|
|
||||||
|
|
||||||
### 3. Processamento no Mercado Pago
|
|
||||||
- Usuário escolhe método (cartão, PIX, boleto)
|
|
||||||
- Completa o pagamento
|
|
||||||
- Mercado Pago processa transação
|
|
||||||
|
|
||||||
### 4. Retorno e Notificação
|
|
||||||
- Usuário retorna para URLs configuradas
|
|
||||||
- Webhooks notificam status via BFF
|
|
||||||
- Sistema atualiza pedido conforme resultado
|
|
||||||
|
|
||||||
## URLs de Retorno
|
|
||||||
|
|
||||||
### Sucesso
|
|
||||||
`https://saveinmed.com.br/pagamento/sucesso`
|
|
||||||
|
|
||||||
**Parâmetros recebidos:**
|
|
||||||
- `payment_id`: ID do pagamento no Mercado Pago
|
|
||||||
- `status`: Status do pagamento (approved, pending, etc.)
|
|
||||||
- `external_reference`: ID do pedido no sistema
|
|
||||||
- `preference_id`: ID da preferência criada
|
|
||||||
|
|
||||||
### Erro
|
|
||||||
`https://saveinmed.com.br/pagamento/erro`
|
|
||||||
|
|
||||||
**Parâmetros recebidos:**
|
|
||||||
- `payment_id`: ID do pagamento (se disponível)
|
|
||||||
- `status`: Status do erro (rejected, cancelled, etc.)
|
|
||||||
- `status_detail`: Detalhes específicos do erro
|
|
||||||
- `external_reference`: ID do pedido
|
|
||||||
|
|
||||||
## Webhook de Notificação
|
|
||||||
|
|
||||||
**Endpoint:** `https://bff.saveinmed.com.br/mercadopago/webhook`
|
|
||||||
|
|
||||||
**Responsabilidade do BFF:**
|
|
||||||
- Receber notificações do Mercado Pago
|
|
||||||
- Validar autenticidade das notificações
|
|
||||||
- Atualizar status dos pagamentos no sistema
|
|
||||||
- Processar ações automáticas (aprovação de pedidos, etc.)
|
|
||||||
|
|
||||||
## Validações Implementadas
|
|
||||||
|
|
||||||
### 1. Dados da Preferência
|
|
||||||
- Pedido ID obrigatório
|
|
||||||
- Pelo menos um item obrigatório
|
|
||||||
- Preços maiores que zero
|
|
||||||
- Quantidades válidas
|
|
||||||
- IDs de produtos presentes
|
|
||||||
|
|
||||||
### 2. Estados do Botão
|
|
||||||
- Desabilitado se dados inválidos
|
|
||||||
- Loading durante criação da preferência
|
|
||||||
- Erro em caso de falha na API
|
|
||||||
|
|
||||||
### 3. Tratamento de Erros
|
|
||||||
- Mensagens específicas por tipo de erro
|
|
||||||
- Sugestões de correção
|
|
||||||
- Informações técnicas para debug
|
|
||||||
|
|
||||||
## Benefícios da Implementação
|
|
||||||
|
|
||||||
### 1. Para o Usuário
|
|
||||||
- Múltiplas opções de pagamento em um só lugar
|
|
||||||
- Interface familiar do Mercado Pago
|
|
||||||
- Segurança e confiabilidade
|
|
||||||
- Parcelamento flexível
|
|
||||||
|
|
||||||
### 2. Para o Sistema
|
|
||||||
- Reduz complexidade de PCI compliance
|
|
||||||
- Aproveitamento da infraestrutura do Mercado Pago
|
|
||||||
- Notificações automáticas de status
|
|
||||||
- Integração via BFF existente
|
|
||||||
|
|
||||||
### 3. Para o Negócio
|
|
||||||
- Maior conversão de vendas
|
|
||||||
- Redução de abandono de carrinho
|
|
||||||
- Suporte a diversos métodos de pagamento
|
|
||||||
- Gestão centralizada via Mercado Pago
|
|
||||||
|
|
||||||
## Configurações Necessárias
|
|
||||||
|
|
||||||
### 1. No BFF
|
|
||||||
- Configurar credenciais do Mercado Pago
|
|
||||||
- Implementar endpoints de preferência e webhook
|
|
||||||
- Validar autenticidade das notificações
|
|
||||||
|
|
||||||
### 2. No Mercado Pago
|
|
||||||
- Configurar URLs de retorno
|
|
||||||
- Configurar URL de webhook
|
|
||||||
- Configurar métodos de pagamento aceitos
|
|
||||||
|
|
||||||
### 3. No Frontend
|
|
||||||
- URLs de sucesso e erro configuradas
|
|
||||||
- Componente de botão implementado
|
|
||||||
- Serviço de API implementado
|
|
||||||
|
|
||||||
## Monitoramento e Debug
|
|
||||||
|
|
||||||
### 1. Logs Implementados
|
|
||||||
- Criação de preferências
|
|
||||||
- Respostas da API
|
|
||||||
- Erros e exceções
|
|
||||||
- Redirecionamentos
|
|
||||||
|
|
||||||
### 2. Informações para Debug
|
|
||||||
- IDs de transação
|
|
||||||
- Status detalhados
|
|
||||||
- Parâmetros de retorno
|
|
||||||
- Dados técnicos nas páginas de erro
|
|
||||||
|
|
||||||
## Próximos Passos
|
|
||||||
|
|
||||||
### 1. Testes
|
|
||||||
- Testar fluxo completo em sandbox
|
|
||||||
- Validar webhooks
|
|
||||||
- Testar cenários de erro
|
|
||||||
|
|
||||||
### 2. Monitoramento
|
|
||||||
- Implementar métricas de conversão
|
|
||||||
- Monitorar erros e falhas
|
|
||||||
- Acompanhar performance
|
|
||||||
|
|
||||||
### 3. Melhorias
|
|
||||||
- Implementar retry automático
|
|
||||||
- Adicionar analytics
|
|
||||||
- Otimizar UX baseado em dados
|
|
||||||
|
|
||||||
## Conclusão
|
|
||||||
|
|
||||||
A implementação do Mercado Pago está completa e seguindo as melhores práticas:
|
|
||||||
|
|
||||||
- ✅ Integração via BFF existente
|
|
||||||
- ✅ Componentes reutilizáveis
|
|
||||||
- ✅ Tratamento completo de erros
|
|
||||||
- ✅ Páginas de retorno implementadas
|
|
||||||
- ✅ Validações robustas
|
|
||||||
- ✅ Logging detalhado
|
|
||||||
- ✅ Interface intuitiva
|
|
||||||
|
|
||||||
O sistema está pronto para testes e deploy, oferecendo uma experiência de pagamento moderna e segura aos usuários do SaveInMed.
|
|
||||||
|
|
@ -1,275 +1,35 @@
|
||||||
# IMPLEMENTAÇÃO DE PAGAMENTOS - CHECKOUT
|
# Implementação de Pagamentos - Checkout
|
||||||
|
|
||||||
## 🎯 Funcionalidade Implementada
|
## Nota de Atualização
|
||||||
|
|
||||||
Integração completa de pagamentos na página `/checkout` com API BFF, incluindo criação de pagamentos e mock de processamento.
|
Este documento descreve um fluxo antigo que citava BFF.
|
||||||
|
|
||||||
## 🛠️ Arquivos Criados/Modificados
|
Estado atual esperado:
|
||||||
|
- o checkout ativo deve usar API direta
|
||||||
|
- referências a `bff-dev.saveinmed.com.br` são históricas
|
||||||
|
- o documento serve como referência funcional, não como contrato de infraestrutura
|
||||||
|
|
||||||
### 1. Novo Serviço de Pagamentos
|
## Resumo
|
||||||
|
|
||||||
#### `src/services/pagamentoApiService.ts`
|
O checkout cobre:
|
||||||
**Funcionalidades principais:**
|
- criação de pagamento
|
||||||
- ✅ **Criar pagamento**: `POST /pagamentos`
|
- confirmação de pagamento
|
||||||
- ✅ **Confirmar pagamento**: `PATCH /pagamentos/:id/confirmar`
|
- associação com pedido
|
||||||
- ✅ **Buscar pagamento**: `GET /pagamentos/:id`
|
- feedback de loading e erro
|
||||||
- ✅ **Listar por pedido**: `GET /pagamentos?pedidos=ID`
|
|
||||||
- ✅ **Mock de processamento**: Simulação temporária
|
|
||||||
|
|
||||||
**Interface de dados:**
|
## Diretriz atual
|
||||||
```typescript
|
|
||||||
interface PagamentoApiData {
|
Os endpoints de pagamento devem sair da API principal, não de BFF.
|
||||||
status: 'pendente' | 'pago' | 'falhou';
|
|
||||||
metodo: 'pix' | 'credito' | 'debito';
|
Exemplo de base esperada:
|
||||||
valor: number;
|
|
||||||
pedidos: string; // ID do pedido
|
```text
|
||||||
}
|
https://api-dev.saveinmed.com.br/api/v1
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Página de Checkout Atualizada
|
## Observação
|
||||||
|
|
||||||
#### `src/app/checkout/page.tsx`
|
Antes de usar este documento para implementação nova:
|
||||||
**Novos estados:**
|
1. validar endpoint real
|
||||||
- `pagamentoId`: ID do pagamento criado
|
2. validar autenticação JWT
|
||||||
- `processandoPagamento`: Loading durante criação
|
3. confirmar shape de request/response
|
||||||
|
|
||||||
**Função atualizada:**
|
|
||||||
- `proximaEtapa()`: Agora async, cria pagamento na etapa 2→3
|
|
||||||
|
|
||||||
## 🔄 Fluxo de Pagamento
|
|
||||||
|
|
||||||
### Etapa 1: Dados de Entrega
|
|
||||||
```
|
|
||||||
Usuário preenche endereço → Clica "Continuar" → Avança para Etapa 2
|
|
||||||
```
|
|
||||||
|
|
||||||
### Etapa 2: Forma de Pagamento
|
|
||||||
```
|
|
||||||
Usuário seleciona método (PIX/Crédito/Débito) →
|
|
||||||
Clica "Continuar" →
|
|
||||||
[NOVO] Cria pagamento na API →
|
|
||||||
[NOVO] Processa mock →
|
|
||||||
Avança para Etapa 3
|
|
||||||
```
|
|
||||||
|
|
||||||
### Etapa 3: Confirmação
|
|
||||||
```
|
|
||||||
Mostra resumo completo + status do pagamento →
|
|
||||||
Usuário confirma pedido final
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Dados Enviados para API
|
|
||||||
|
|
||||||
### Payload de Criação
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "pendente",
|
|
||||||
"metodo": "pix", // ou "credito", "debito"
|
|
||||||
"valor": 71.85,
|
|
||||||
"pedidos": "68efaeb000194d1a94bf"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mapeamento de Métodos
|
|
||||||
- **Interface → API**:
|
|
||||||
- `"cartao"` → `"credito"`
|
|
||||||
- `"pix"` → `"pix"`
|
|
||||||
- `"boleto"` → `"debito"`
|
|
||||||
|
|
||||||
### Resposta da API
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"$id": "pagamento_id_123456",
|
|
||||||
"status": "pendente",
|
|
||||||
"metodo": "pix",
|
|
||||||
"valor": 71.85,
|
|
||||||
"pedidos": "68efaeb000194d1a94bf",
|
|
||||||
"$createdAt": "2025-10-15T10:30:00.000Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎭 Mock de Processamento
|
|
||||||
|
|
||||||
### Funcionalidade Temporária
|
|
||||||
```typescript
|
|
||||||
processarPagamentoMock: async (metodo, valor) => {
|
|
||||||
// Simula delay de 2 segundos
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
// 90% de sucesso simulado
|
|
||||||
const sucesso = Math.random() > 0.1;
|
|
||||||
|
|
||||||
if (sucesso) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
transactionId: `mock_${Date.now()}_${randomId}`
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Falha na simulação do pagamento'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Objetivo**: Simular processamento real até integração com gateway de pagamento.
|
|
||||||
|
|
||||||
## 🎨 Interface Atualizada
|
|
||||||
|
|
||||||
### Botão "Continuar" na Etapa 2
|
|
||||||
#### Estado Normal:
|
|
||||||
```jsx
|
|
||||||
<button>Continuar</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Estado Loading:
|
|
||||||
```jsx
|
|
||||||
<button disabled>
|
|
||||||
<spinner /> Processando Pagamento...
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Etapa 3 - Confirmação
|
|
||||||
#### Informações do Pagamento:
|
|
||||||
```
|
|
||||||
Forma de Pagamento
|
|
||||||
├── Método: PIX
|
|
||||||
├── Valor: R$ 71,85
|
|
||||||
└── ID do Pagamento: 68efaeb00019...
|
|
||||||
|
|
||||||
Status do Pagamento
|
|
||||||
✅ Pagamento criado com sucesso!
|
|
||||||
Status: Pendente - Aguardando confirmação
|
|
||||||
💡 Para PIX: Escaneie o QR Code ou copie o código quando disponível
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Logs de Debug
|
|
||||||
|
|
||||||
### Console Output Esperado
|
|
||||||
```
|
|
||||||
💳 [CHECKOUT] Criando pagamento: {metodo: "pix", valor: 71.85, pedidoId: "..."}
|
|
||||||
💳 Criando pagamento na API: {status: "pendente", metodo: "pix", valor: 71.85, pedidos: "..."}
|
|
||||||
✅ Pagamento criado com sucesso: {$id: "...", status: "pendente", ...}
|
|
||||||
🎭 [CHECKOUT] Iniciando processamento mock...
|
|
||||||
🎭 MOCK: Processando pagamento... {metodo: "pix", valor: 71.85}
|
|
||||||
✅ MOCK: Pagamento processado com sucesso: mock_1729012345_abc123
|
|
||||||
✅ [CHECKOUT] Pagamento processado (mock): mock_1729012345_abc123
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 Requisições de Rede
|
|
||||||
|
|
||||||
### Criação de Pagamento
|
|
||||||
```
|
|
||||||
POST https://bff-dev.saveinmed.com.br/api/v1/pagamentos
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"status": "pendente",
|
|
||||||
"metodo": "pix",
|
|
||||||
"valor": 71.85,
|
|
||||||
"pedidos": "68efaeb000194d1a94bf"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Futura Confirmação (endpoint disponível)
|
|
||||||
```
|
|
||||||
PATCH https://bff-dev.saveinmed.com.br/api/v1/pagamentos/{id}/confirmar
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛡️ Tratamento de Erros
|
|
||||||
|
|
||||||
### Cenários Cobertos
|
|
||||||
1. **Token ausente/inválido** → Erro de autenticação
|
|
||||||
2. **Pedido ID não encontrado** → Erro antes da criação
|
|
||||||
3. **Falha na API** → Mensagem de erro específica
|
|
||||||
4. **Falha no mock** → Simulação de erro de pagamento
|
|
||||||
5. **Erro de rede** → Erro de conexão
|
|
||||||
|
|
||||||
### Mensagens de Feedback
|
|
||||||
- ✅ **Sucesso**: "Pagamento criado com sucesso!"
|
|
||||||
- ✅ **Mock sucesso**: "Pagamento processado! ID: mock_12345..."
|
|
||||||
- ❌ **Erro API**: "Erro ao criar pagamento: [mensagem]"
|
|
||||||
- ❌ **Erro mock**: "Falha no processamento do pagamento (simulação)"
|
|
||||||
- ❌ **Erro geral**: "Erro ao processar pagamento. Tente novamente."
|
|
||||||
|
|
||||||
## 🧪 Como Testar
|
|
||||||
|
|
||||||
### Teste Completo do Fluxo
|
|
||||||
1. **Adicione produtos** ao carrinho
|
|
||||||
2. **Clique "Finalizar Compra"** (cria pedido)
|
|
||||||
3. **Preencha dados de entrega** e clique "Continuar"
|
|
||||||
4. **Selecione método de pagamento** (PIX/Crédito/Débito)
|
|
||||||
5. **Clique "Continuar"** → Observe criação do pagamento
|
|
||||||
6. **Aguarde processamento mock** (2 segundos)
|
|
||||||
7. **Verifique informações** na etapa de confirmação
|
|
||||||
|
|
||||||
### Verificações no Network
|
|
||||||
- ✅ `POST /pagamentos` → Status 200
|
|
||||||
- ✅ Body com dados corretos
|
|
||||||
- ✅ Response com ID do pagamento
|
|
||||||
|
|
||||||
### Verificações na Interface
|
|
||||||
- ✅ Loading no botão durante processamento
|
|
||||||
- ✅ Toast de sucesso/erro
|
|
||||||
- ✅ ID do pagamento na confirmação
|
|
||||||
- ✅ Status "Pendente" exibido
|
|
||||||
|
|
||||||
## 📱 Estados da Interface
|
|
||||||
|
|
||||||
### Durante Criação de Pagamento
|
|
||||||
```
|
|
||||||
[Botão] ⟳ Processando Pagamento... (disabled)
|
|
||||||
[Toast] 💳 Criando pagamento...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Após Sucesso
|
|
||||||
```
|
|
||||||
[Toast] ✅ Pagamento criado com sucesso!
|
|
||||||
[Toast] ✅ Pagamento processado! ID: mock_...
|
|
||||||
[Tela] Avança para etapa 3 com informações do pagamento
|
|
||||||
```
|
|
||||||
|
|
||||||
### Após Erro
|
|
||||||
```
|
|
||||||
[Toast] ❌ Erro ao criar pagamento: [detalhes]
|
|
||||||
[Botão] Volta ao estado normal para retry
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔮 Próximos Passos
|
|
||||||
|
|
||||||
### Integração Real de Pagamento
|
|
||||||
1. **Substituir mock** por gateway real (Stripe, PagSeguro, etc.)
|
|
||||||
2. **Implementar webhooks** para confirmação automática
|
|
||||||
3. **Adicionar QR Code** para PIX
|
|
||||||
4. **Implementar retry** de pagamentos falhados
|
|
||||||
|
|
||||||
### Funcionalidades Adicionais
|
|
||||||
1. **Histórico de pagamentos** por pedido
|
|
||||||
2. **Cancelamento de pagamentos**
|
|
||||||
3. **Reembolsos** via API
|
|
||||||
4. **Notificações** de status
|
|
||||||
|
|
||||||
## ✅ Resultados
|
|
||||||
|
|
||||||
### Funcionalidade
|
|
||||||
- ✅ **API BFF integrada** para pagamentos
|
|
||||||
- ✅ **Fluxo completo** implementado
|
|
||||||
- ✅ **Mock funcional** até integração real
|
|
||||||
- ✅ **Estados de loading** e feedback
|
|
||||||
|
|
||||||
### UX/UI
|
|
||||||
- ✅ **Feedback visual** durante processamento
|
|
||||||
- ✅ **Informações claras** na confirmação
|
|
||||||
- ✅ **Tratamento de erros** robusto
|
|
||||||
- ✅ **Flow intuitivo** para o usuário
|
|
||||||
|
|
||||||
### Integração
|
|
||||||
- ✅ **Dados consistentes** entre pedido e pagamento
|
|
||||||
- ✅ **Autenticação** via JWT
|
|
||||||
- ✅ **Logs detalhados** para debug
|
|
||||||
- ✅ **Preparado** para gateway real
|
|
||||||
|
|
||||||
O sistema de pagamentos está **100% funcional** com mock e pronto para integração real com gateways de pagamento! 🚀
|
|
||||||
|
|
@ -3,121 +3,121 @@
|
||||||
## Status (pronto x faltando)
|
## Status (pronto x faltando)
|
||||||
|
|
||||||
**Pronto**
|
**Pronto**
|
||||||
- Conteúdo descrito neste documento.
|
- Conteúdo descrito neste documento.
|
||||||
|
|
||||||
**Faltando**
|
**Faltando**
|
||||||
- Confirmar no código o estado real das funcionalidades e atualizar esta seção conforme necessário.
|
- Confirmar no código o estado real das funcionalidades e atualizar esta seção conforme necessário.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
Interface do usuário do marketplace B2B farmacêutico SaveInMed, desenvolvida com React e Vite.
|
Interface do usuário do marketplace B2B farmacêutico SaveInMed, desenvolvida com React e Vite.
|
||||||
|
|
||||||
## 🎯 Propósito
|
## 🎯 Propósito
|
||||||
|
|
||||||
Este é o frontend do marketplace SaveInMed, onde farmácias e distribuidoras podem:
|
Este é o frontend do marketplace SaveInMed, onde farmácias e distribuidoras podem:
|
||||||
- Navegar e pesquisar produtos farmacêuticos
|
- Navegar e pesquisar produtos farmacêuticos
|
||||||
- Adicionar produtos ao carrinho
|
- Adicionar produtos ao carrinho
|
||||||
- Realizar pedidos e checkout
|
- Realizar pedidos e checkout
|
||||||
- Gerenciar perfil e histórico de compras
|
- Gerenciar perfil e histórico de compras
|
||||||
- Acompanhar status de pedidos
|
- Acompanhar status de pedidos
|
||||||
- Processar pagamentos via Mercado Pago
|
- Processar pagamentos via Mercado Pago
|
||||||
|
|
||||||
## 🚀 Tecnologias
|
## 🚀 Tecnologias
|
||||||
|
|
||||||
- **React 18** - Biblioteca UI
|
- **React 18** - Biblioteca UI
|
||||||
- **TypeScript** - Tipagem estática
|
- **TypeScript** - Tipagem estática
|
||||||
- **Vite 5** - Build tool e dev server ultra-rápido
|
- **Vite 5** - Build tool e dev server ultra-rápido
|
||||||
- **React Router DOM 6** - Roteamento
|
- **React Router DOM 6** - Roteamento
|
||||||
- **TailwindCSS 3** - Framework CSS utility-first
|
- **TailwindCSS 3** - Framework CSS utility-first
|
||||||
- **Zustand 4** - Gerenciamento de estado
|
- **Zustand 4** - Gerenciamento de estado
|
||||||
- **Axios** - Cliente HTTP
|
- **Axios** - Cliente HTTP
|
||||||
- **Mercado Pago SDK React** - Integração de pagamentos
|
- **Mercado Pago SDK React** - Integração de pagamentos
|
||||||
- **React Window** - Virtualização de listas para performance
|
- **React Window** - Virtualização de listas para performance
|
||||||
|
|
||||||
## 📋 Funcionalidades
|
## 📋 Funcionalidades
|
||||||
|
|
||||||
### Catálogo de Produtos
|
### Catálogo de Produtos
|
||||||
- Listagem de produtos com virtualização
|
- Listagem de produtos com virtualização
|
||||||
- Busca e filtros avançados
|
- Busca e filtros avançados
|
||||||
- Detalhes de produtos
|
- Detalhes de produtos
|
||||||
- Informações de lote e validade
|
- Informações de lote e validade
|
||||||
|
|
||||||
### Carrinho de Compras
|
### Carrinho de Compras
|
||||||
- Adicionar/remover produtos
|
- Adicionar/remover produtos
|
||||||
- Atualizar quantidades
|
- Atualizar quantidades
|
||||||
- Persistência local com Zustand
|
- Persistência local com Zustand
|
||||||
- Cálculo automático de totais
|
- Cálculo automático de totais
|
||||||
|
|
||||||
### Checkout e Pagamentos
|
### Checkout e Pagamentos
|
||||||
- Fluxo de checkout simplificado
|
- Fluxo de checkout simplificado
|
||||||
- Integração com Mercado Pago
|
- Integração com Mercado Pago
|
||||||
- Múltiplas formas de pagamento
|
- Múltiplas formas de pagamento
|
||||||
- Confirmação de pedido
|
- Confirmação de pedido
|
||||||
|
|
||||||
### Autenticação
|
### Autenticação
|
||||||
- Login de usuários
|
- Login de usuários
|
||||||
- Rotas protegidas
|
- Rotas protegidas
|
||||||
- Contexto de autenticação
|
- Contexto de autenticação
|
||||||
- Persistência de sessão
|
- Persistência de sessão
|
||||||
|
|
||||||
### Dashboard
|
### Dashboard
|
||||||
- Visão geral de pedidos
|
- Visão geral de pedidos
|
||||||
- Histórico de compras
|
- Histórico de compras
|
||||||
- Estatísticas de uso
|
- EstatÃsticas de uso
|
||||||
|
|
||||||
### Perfil
|
### Perfil
|
||||||
- Gerenciamento de dados pessoais
|
- Gerenciamento de dados pessoais
|
||||||
- Endereços de entrega
|
- Endereços de entrega
|
||||||
- Preferências
|
- Preferências
|
||||||
|
|
||||||
## 🏗️ Arquitetura
|
## ðŸ—ï¸ Arquitetura
|
||||||
|
|
||||||
```
|
```
|
||||||
marketplace/
|
marketplace/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ └── ProtectedRoute.tsx # Componente de rota protegida
|
│ │ └── ProtectedRoute.tsx # Componente de rota protegida
|
||||||
│ ├── context/
|
│ ├── context/
|
||||||
│ │ └── AuthContext.tsx # Contexto de autenticação
|
│ │ └── AuthContext.tsx # Contexto de autenticação
|
||||||
│ ├── hooks/
|
│ ├── hooks/
|
||||||
│ │ └── usePersistentFilters.ts # Hook para filtros persistentes
|
│ │ └── usePersistentFilters.ts # Hook para filtros persistentes
|
||||||
│ ├── layouts/
|
│ ├── layouts/
|
||||||
│ │ └── Shell.tsx # Layout principal
|
│ │ └── Shell.tsx # Layout principal
|
||||||
│ ├── pages/
|
│ ├── pages/
|
||||||
│ │ ├── Cart.tsx # Página do carrinho
|
│ │ ├── Cart.tsx # Página do carrinho
|
||||||
│ │ ├── Checkout.tsx # Página de checkout
|
│ │ ├── Checkout.tsx # Página de checkout
|
||||||
│ │ ├── Company.tsx # Perfil da empresa [NEW]
|
│ │ ├── Company.tsx # Perfil da empresa [NEW]
|
||||||
│ │ ├── Dashboard.tsx # Dashboard do usuário
|
│ │ ├── Dashboard.tsx # Dashboard do usuário
|
||||||
│ │ ├── Inventory.tsx # Gestão de estoque [NEW]
|
│ │ ├── Inventory.tsx # Gestão de estoque [NEW]
|
||||||
│ │ ├── Login.tsx # Página de login
|
│ │ ├── Login.tsx # Página de login
|
||||||
│ │ ├── Orders.tsx # Pedidos [NEW]
|
│ │ ├── Orders.tsx # Pedidos [NEW]
|
||||||
│ │ ├── Profile.tsx # Página de perfil
|
│ │ ├── Profile.tsx # Página de perfil
|
||||||
│ │ └── SellerDashboard.tsx # Dashboard vendedor [NEW]
|
│ │ └── SellerDashboard.tsx # Dashboard vendedor [NEW]
|
||||||
│ ├── services/
|
│ ├── services/
|
||||||
│ │ └── apiClient.ts # Cliente API configurado
|
│ │ └── apiClient.ts # Cliente API configurado
|
||||||
│ ├── stores/
|
│ ├── stores/
|
||||||
│ │ └── cartStore.ts # Store Zustand do carrinho
|
│ │ └── cartStore.ts # Store Zustand do carrinho
|
||||||
│ ├── test/
|
│ ├── test/
|
||||||
│ │ └── setup.ts # Setup Vitest
|
│ │ └── setup.ts # Setup Vitest
|
||||||
│ ├── types/
|
│ ├── types/
|
||||||
│ │ └── product.ts # Tipos TypeScript
|
│ │ └── product.ts # Tipos TypeScript
|
||||||
│ ├── App.tsx # Componente raiz
|
│ ├── App.tsx # Componente raiz
|
||||||
│ ├── main.tsx # Entry point
|
│ ├── main.tsx # Entry point
|
||||||
│ └── index.css # Estilos globais
|
│ └── index.css # Estilos globais
|
||||||
├── index.html
|
├── index.html
|
||||||
├── vite.config.ts
|
├── vite.config.ts
|
||||||
├── vitest.config.ts # Config de testes
|
├── vitest.config.ts # Config de testes
|
||||||
├── tailwind.config.ts
|
├── tailwind.config.ts
|
||||||
├── tsconfig.json
|
├── tsconfig.json
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎨 Assets
|
## 🎨 Assets
|
||||||
|
|
||||||
Os arquivos de assets estão em `src/assets/`:
|
Os arquivos de assets estão em `src/assets/`:
|
||||||
|
|
||||||
| Arquivo | Descrição |
|
| Arquivo | Descrição |
|
||||||
|---------|-----------|
|
|---------|-----------|
|
||||||
| `logo.png` | Logo oficial SaveInMed |
|
| `logo.png` | Logo oficial SaveInMed |
|
||||||
|
|
||||||
|
|
@ -131,9 +131,9 @@ colors: {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🧪 Testes
|
## 🧪 Testes
|
||||||
|
|
||||||
O projeto utiliza **Vitest** para testes unitários:
|
O projeto utiliza **Vitest** para testes unitários:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Executar testes
|
# Executar testes
|
||||||
|
|
@ -150,14 +150,14 @@ npm test -- --run
|
||||||
|
|
||||||
| Categoria | Testes |
|
| Categoria | Testes |
|
||||||
|-----------|--------|
|
|-----------|--------|
|
||||||
| `cartStore` | 15 ✅ |
|
| `cartStore` | 15 ✅ |
|
||||||
| `apiClient` | 7 ✅ |
|
| `apiClient` | 7 ✅ |
|
||||||
| `usePersistentFilters` | 5 ✅ |
|
| `usePersistentFilters` | 5 ✅ |
|
||||||
| **Total** | **27** ✅ |
|
| **Total** | **27** ✅ |
|
||||||
|
|
||||||
## 🔧 Configuração
|
## 🔧 Configuração
|
||||||
|
|
||||||
### Variáveis de Ambiente
|
### Variáveis de Ambiente
|
||||||
|
|
||||||
Crie um arquivo `.env` na raiz do projeto:
|
Crie um arquivo `.env` na raiz do projeto:
|
||||||
|
|
||||||
|
|
@ -172,43 +172,43 @@ VITE_MERCADOPAGO_PUBLIC_KEY=your-public-key-here
|
||||||
VITE_ENV=development
|
VITE_ENV=development
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pré-requisitos
|
### Pré-requisitos
|
||||||
|
|
||||||
- Node.js 20+
|
- Node.js 20+
|
||||||
- npm ou yarn
|
- npm ou yarn
|
||||||
|
|
||||||
## 🏃 Execução Local
|
## 🃠Execução Local
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Instalar dependências
|
# Instalar dependências
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Modo desenvolvimento
|
# Modo desenvolvimento
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
# Aplicação estará disponível em http://localhost:5173
|
# Aplicação estará disponÃvel em http://localhost:5173
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🏗️ Build e Produção
|
## ðŸ—ï¸ Build e Produção
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build para produção
|
# Build para produção
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# Preview do build
|
# Preview do build
|
||||||
npm run preview
|
npm run preview
|
||||||
|
|
||||||
# Arquivos de produção estarão em ./dist
|
# Arquivos de produção estarão em ./dist
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎨 Estilização
|
## 🎨 Estilização
|
||||||
|
|
||||||
O projeto utiliza TailwindCSS para estilização. Principais recursos:
|
O projeto utiliza TailwindCSS para estilização. Principais recursos:
|
||||||
|
|
||||||
- **Utility-first**: Classes utilitárias para estilização rápida
|
- **Utility-first**: Classes utilitárias para estilização rápida
|
||||||
- **Responsivo**: Design mobile-first
|
- **Responsivo**: Design mobile-first
|
||||||
- **Dark mode**: Suporte a tema escuro (se implementado)
|
- **Dark mode**: Suporte a tema escuro (se implementado)
|
||||||
- **Customização**: Configuração em `tailwind.config.ts`
|
- **Customização**: Configuração em `tailwind.config.ts`
|
||||||
|
|
||||||
### Exemplo de Componente
|
### Exemplo de Componente
|
||||||
|
|
||||||
|
|
@ -231,9 +231,9 @@ export function ProductCard({ product }: { product: Product }) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔐 Autenticação
|
## 🔠Autenticação
|
||||||
|
|
||||||
O sistema de autenticação utiliza Context API:
|
O sistema de autenticação utiliza Context API:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useAuth } from './context/AuthContext';
|
import { useAuth } from './context/AuthContext';
|
||||||
|
|
@ -253,9 +253,9 @@ function MyComponent() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛒 Gerenciamento de Estado (Zustand)
|
## 🛒 Gerenciamento de Estado (Zustand)
|
||||||
|
|
||||||
O carrinho de compras é gerenciado com Zustand:
|
O carrinho de compras é gerenciado com Zustand:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useCartStore } from './stores/cartStore';
|
import { useCartStore } from './stores/cartStore';
|
||||||
|
|
@ -271,7 +271,7 @@ function ProductPage() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🧪 Testes
|
## 🧪 Testes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Executar testes (quando configurados)
|
# Executar testes (quando configurados)
|
||||||
|
|
@ -281,25 +281,25 @@ npm test
|
||||||
npm run test:coverage
|
npm run test:coverage
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📱 Responsividade
|
## 📱 Responsividade
|
||||||
|
|
||||||
O marketplace é totalmente responsivo, com breakpoints:
|
O marketplace é totalmente responsivo, com breakpoints:
|
||||||
|
|
||||||
- **Mobile**: < 640px
|
- **Mobile**: < 640px
|
||||||
- **Tablet**: 640px - 1024px
|
- **Tablet**: 640px - 1024px
|
||||||
- **Desktop**: > 1024px
|
- **Desktop**: > 1024px
|
||||||
|
|
||||||
## ⚡ Performance
|
## âš¡ Performance
|
||||||
|
|
||||||
Otimizações implementadas:
|
Otimizações implementadas:
|
||||||
|
|
||||||
- **Virtualização de listas**: React Window para listas longas
|
- **Virtualização de listas**: React Window para listas longas
|
||||||
- **Code splitting**: Lazy loading de rotas
|
- **Code splitting**: Lazy loading de rotas
|
||||||
- **Vite**: Build ultra-rápido
|
- **Vite**: Build ultra-rápido
|
||||||
- **Zustand**: State management leve e performático
|
- **Zustand**: State management leve e performático
|
||||||
- **Memoização**: React.memo e useMemo onde apropriado
|
- **Memoização**: React.memo e useMemo onde apropriado
|
||||||
|
|
||||||
## 🔗 Integração com APIs
|
## 🔗 Integração com APIs
|
||||||
|
|
||||||
### Backend Go API
|
### Backend Go API
|
||||||
```typescript
|
```typescript
|
||||||
|
|
@ -323,7 +323,7 @@ function CheckoutButton({ preferenceId }: { preferenceId: string }) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Deploy
|
## 🚀 Deploy
|
||||||
|
|
||||||
### Vercel
|
### Vercel
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -346,12 +346,12 @@ docker build -t saveinmed-marketplace:latest .
|
||||||
docker run -p 80:80 saveinmed-marketplace:latest
|
docker run -p 80:80 saveinmed-marketplace:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔗 Integração com Outros Componentes
|
## 🔗 Integração com Outros Componentes
|
||||||
|
|
||||||
- **Backend (Go API)**: Consome endpoints de produtos, pedidos e pagamentos
|
- **Backend (Go API)**: Consome endpoints de produtos, pedidos e pagamentos
|
||||||
- **Backoffice (NestJS)**: Consome endpoints administrativos
|
- **Backoffice (NestJS)**: Consome endpoints administrativos
|
||||||
- **SaveInMed BFF**: Pode usar como proxy para múltiplas APIs
|
- **SaveInMed BFF**: Pode usar como proxy para múltiplas APIs
|
||||||
|
|
||||||
## 📝 Licença
|
## 📠Licença
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,37 @@
|
||||||
# Frontend — SaveInMed Marketplace
|
# Frontend - SaveInMed Marketplace
|
||||||
|
|
||||||
React + Vite + TypeScript + Tailwind CSS
|
React + Vite + TypeScript + Tailwind CSS
|
||||||
|
|
||||||
|
## Estado Atual
|
||||||
|
|
||||||
|
- Fluxo ativo de autenticação: API direta via `VITE_API_URL`
|
||||||
|
- Fluxo ativo não depende de BFF
|
||||||
|
- Papéis suportados no frontend ativo: `admin`, `owner`, `employee`, `delivery`
|
||||||
|
- `superadmin` e aliases antigos (`seller`, `colaborador`, `entregador`, `dono`) existem apenas como compatibilidade de legado
|
||||||
|
|
||||||
## Estrutura de `src/`
|
## Estrutura de `src/`
|
||||||
|
|
||||||
```
|
```text
|
||||||
src/
|
src/
|
||||||
├── assets/ → Logo, imagens estáticas
|
|-- assets/ -> Logo, imagens estáticas
|
||||||
├── components/ → Componentes reutilizáveis
|
|-- components/ -> Componentes reutilizáveis
|
||||||
├── context/ → React Context (auth, tema) — estado global estável
|
|-- context/ -> React Context (auth, tema)
|
||||||
├── hooks/ → Custom hooks
|
|-- hooks/ -> Custom hooks
|
||||||
├── layouts/ → Layouts de página (Shell, DashboardLayout)
|
|-- layouts/ -> Layouts de página
|
||||||
├── pages/ → Páginas organizadas por contexto e perfil
|
|-- pages/ -> Páginas organizadas por contexto e perfil
|
||||||
│ ├── auth/ → Login
|
| |-- auth/ -> Login
|
||||||
│ ├── marketplace/ → Search, Cart, Checkout, Orders, OrderDetails
|
| |-- marketplace/ -> Search, Cart, Checkout, Orders, OrderDetails
|
||||||
│ └── dashboard/
|
| `-- dashboard/
|
||||||
│ ├── admin/ → Painel administrativo (role admin)
|
| |-- admin/ -> Painel administrativo
|
||||||
│ ├── seller/ → Inventário, Produtos, Carteira, Equipe
|
| |-- seller/ -> Área do `owner` (nome de pasta legado)
|
||||||
│ ├── employee/ → Dashboard do colaborador
|
| |-- employee/ -> Área do `employee`
|
||||||
│ ├── delivery/ → Dashboard do entregador
|
| |-- delivery/ -> Área do `delivery`
|
||||||
│ ├── Company.tsx
|
| |-- Company.tsx
|
||||||
│ └── MyProfile.tsx
|
| `-- MyProfile.tsx
|
||||||
├── services/ → Clientes HTTP por domínio
|
|-- services/ -> Clientes HTTP por domínio
|
||||||
├── stores/ → Zustand (carrinho, filtros, UI global)
|
|-- stores/ -> Zustand (carrinho, filtros, UI global)
|
||||||
├── types/ → Tipos TypeScript compartilhados
|
|-- types/ -> Tipos TypeScript compartilhados
|
||||||
└── utils/ → format, jwt, logger
|
`-- utils/ -> format, jwt, logger
|
||||||
```
|
```
|
||||||
|
|
||||||
## Importações absolutas
|
## Importações absolutas
|
||||||
|
|
@ -32,30 +39,51 @@ src/
|
||||||
Utilize o alias `@/` (aponta para `src/`):
|
Utilize o alias `@/` (aponta para `src/`):
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// ✅ Correto
|
|
||||||
import { useAuth } from '@/context/AuthContext'
|
import { useAuth } from '@/context/AuthContext'
|
||||||
|
|
||||||
// ❌ Evitar (quebra ao mover o arquivo)
|
|
||||||
import { useAuth } from '../../context/AuthContext'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Gerenciamento de estado
|
## Gerenciamento de estado
|
||||||
|
|
||||||
| Estado | Estratégia |
|
| Estado | Estratégia |
|
||||||
|--------|-----------|
|
|--------|-----------|
|
||||||
| Auth (JWT, user) | Context API (`AuthContext`) |
|
| Auth (JWT, user) | Context API (`AuthContext`) + `apiClient` |
|
||||||
| Tema claro/escuro | Context API (`ThemeContext`) |
|
| Tema claro/escuro | Context API (`ThemeContext`) |
|
||||||
| Carrinho | Zustand (`cartStore`) |
|
| Carrinho | Zustand (`cartStore`) |
|
||||||
| Filtros de busca | Zustand / `usePersistentFilters` |
|
| Filtros de busca | Zustand / `usePersistentFilters` |
|
||||||
| Estado UI local | `useState` no componente |
|
| Estado UI local | `useState` no componente |
|
||||||
|
|
||||||
> **Regra:** Context apenas para auth e tema. Tudo mais vai para Zustand.
|
> Regra: Context apenas para auth e tema. O restante fica em Zustand ou estado local.
|
||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev # Dev server (proxy → :8214)
|
pnpm dev
|
||||||
pnpm build # Build produção
|
pnpm build
|
||||||
pnpm test # Testes (Vitest)
|
pnpm test
|
||||||
pnpm lint # ESLint
|
pnpm lint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Autenticação e API
|
||||||
|
|
||||||
|
- O app Vite usa [src/services/apiClient.ts](./src/services/apiClient.ts)
|
||||||
|
- O base URL do fluxo ativo vem de `VITE_API_URL`
|
||||||
|
- Exemplo esperado:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VITE_API_URL=https://api-dev.saveinmed.com.br
|
||||||
|
```
|
||||||
|
|
||||||
|
- O login ativo chama `POST /v1/auth/login`
|
||||||
|
- O frontend injeta o token JWT em `Authorization: Bearer <token>`
|
||||||
|
- Não usar `NEXT_PUBLIC_BFF_API_URL` para o app Vite ativo
|
||||||
|
|
||||||
|
## Roles suportadas
|
||||||
|
|
||||||
|
- `admin`
|
||||||
|
- `owner`
|
||||||
|
- `employee`
|
||||||
|
- `delivery`
|
||||||
|
|
||||||
|
## Observação sobre legado
|
||||||
|
|
||||||
|
O repositório ainda contém páginas e serviços legados em `src/app/...` e stubs antigos. Esses arquivos não devem ser usados como referência de arquitetura nova sem revisão.
|
||||||
|
|
@ -6,11 +6,7 @@ import { useEmpresa } from "@/contexts/EmpresaContext";
|
||||||
import { translateError } from "@/lib/error-translator";
|
import { translateError } from "@/lib/error-translator";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
const BFF_BASE_URL =
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ? process.env.NEXT_PUBLIC_API_URL + '/api/v1' : 'https://api-dev.saveinmed.com.br/api/v1';
|
||||||
process.env.NEXT_PUBLIC_BFF_API_URL ||
|
|
||||||
(process.env.NEXT_PUBLIC_API_URL
|
|
||||||
? `${process.env.NEXT_PUBLIC_API_URL}/api/v1`
|
|
||||||
: "https://api-dev.saveinmed.com.br/api/v1");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Componente interno que usa useSearchParams
|
* Componente interno que usa useSearchParams
|
||||||
|
|
@ -20,21 +16,21 @@ const LoginPageContent = () => {
|
||||||
const { setEmpresaId } = useEmpresa();
|
const { setEmpresaId } = useEmpresa();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
// Estados do formulário
|
// Estados do formulário
|
||||||
const [email, setEmail] = useState<string>(""); // Email do usuário
|
const [email, setEmail] = useState<string>(""); // Email do usuário
|
||||||
const [password, setPassword] = useState<string>(""); // Senha do usuário
|
const [password, setPassword] = useState<string>(""); // Senha do usuário
|
||||||
const [name, setName] = useState<string>(""); // Nome completo (apenas para registro)
|
const [name, setName] = useState<string>(""); // Nome completo (apenas para registro)
|
||||||
const [loading, setLoading] = useState<boolean>(false); // Estado de carregamento
|
const [loading, setLoading] = useState<boolean>(false); // Estado de carregamento
|
||||||
const [error, setError] = useState<string>(""); // Mensagens de erro
|
const [error, setError] = useState<string>(""); // Mensagens de erro
|
||||||
const [isLogin, setIsLogin] = useState<boolean>(true); // Alterna entre login e registro
|
const [isLogin, setIsLogin] = useState<boolean>(true); // Alterna entre login e registro
|
||||||
const [checkingAuth, setCheckingAuth] = useState<boolean>(true); // Verificação inicial de autenticação
|
const [checkingAuth, setCheckingAuth] = useState<boolean>(true); // Verificação inicial de autenticação
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false); // Controla visibilidade da senha
|
const [showPassword, setShowPassword] = useState<boolean>(false); // Controla visibilidade da senha
|
||||||
const [accessToken, setAccessToken] = useState<string>(""); // Token de acesso do BFF
|
const [accessToken, setAccessToken] = useState<string>(""); // Token de acesso do BFF
|
||||||
const [showSuccessModal, setShowSuccessModal] = useState<boolean>(false); // Modal de sucesso do cadastro
|
const [showSuccessModal, setShowSuccessModal] = useState<boolean>(false); // Modal de sucesso do cadastro
|
||||||
const [showPendingModal, setShowPendingModal] = useState<boolean>(false); // Modal de cadastro pendente
|
const [showPendingModal, setShowPendingModal] = useState<boolean>(false); // Modal de cadastro pendente
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifica parâmetros da URL para definir aba inicial
|
* Verifica parâmetros da URL para definir aba inicial
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tab = searchParams.get('tab');
|
const tab = searchParams.get('tab');
|
||||||
|
|
@ -44,13 +40,13 @@ const LoginPageContent = () => {
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifica se o usuário já está autenticado ao carregar a página
|
* Verifica se o usuário já está autenticado ao carregar a página
|
||||||
* Se estiver autenticado, redireciona para o dashboard
|
* Se estiver autenticado, redireciona para o dashboard
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuth = async () => {
|
const checkAuth = async () => {
|
||||||
try {
|
try {
|
||||||
// Verificar se há token armazenado
|
// Verificar se há token armazenado
|
||||||
const storedToken = localStorage.getItem('access_token');
|
const storedToken = localStorage.getItem('access_token');
|
||||||
|
|
||||||
if (!storedToken) {
|
if (!storedToken) {
|
||||||
|
|
@ -58,14 +54,14 @@ const LoginPageContent = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar autenticação usando BFF com o token no header Authorization
|
// Verificar autenticação usando API com o token no header Authorization
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${BFF_BASE_URL}/auth/me`,
|
`${API_BASE_URL}/auth/me`,
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"accept": "application/json",
|
"accept": "application/json",
|
||||||
"Authorization": `Bearer ${storedToken}`, // Usar Authorization ao invés de Cookie
|
"Authorization": `Bearer ${storedToken}`, // Usar Authorization ao invés de Cookie
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -76,7 +72,7 @@ const LoginPageContent = () => {
|
||||||
router.push("/dashboard");
|
router.push("/dashboard");
|
||||||
} else {
|
} else {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
// Limpar token inválido
|
// Limpar token inválido
|
||||||
localStorage.removeItem('access_token');
|
localStorage.removeItem('access_token');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -88,17 +84,17 @@ const LoginPageContent = () => {
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Função para realizar login do usuário usando BFF
|
* Função para realizar login do usuário usando API
|
||||||
* @param email - Email do usuário (usado como identificador)
|
* @param email - Email do usuário (usado como identificador)
|
||||||
* @param password - Senha do usuário
|
* @param password - Senha do usuário
|
||||||
*/
|
*/
|
||||||
const login = async (email: string, password: string) => {
|
const login = async (email: string, password: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// 1. Fazer login no BFF
|
// 1. Fazer login na API
|
||||||
const baseUrl = BFF_BASE_URL;
|
const baseUrl = API_BASE_URL;
|
||||||
const response = await fetch(`${baseUrl}/auth/login`, {
|
const response = await fetch(`${baseUrl}/auth/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -113,32 +109,32 @@ const LoginPageContent = () => {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ler e logar o corpo da resposta (usando clone para não consumir o body original)
|
// Ler e logar o corpo da resposta (usando clone para não consumir o body original)
|
||||||
const respClone = response.clone();
|
const respClone = response.clone();
|
||||||
const respText = await respClone.text();
|
const respText = await respClone.text();
|
||||||
let respBody: any = respText;
|
let respBody: any = respText;
|
||||||
try {
|
try {
|
||||||
respBody = JSON.parse(respText);
|
respBody = JSON.parse(respText);
|
||||||
} catch {
|
} catch {
|
||||||
// corpo não é JSON, manter como texto
|
// corpo não é JSON, manter como texto
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// Extrair mensagem de erro do backend
|
// Extrair mensagem de erro do backend
|
||||||
let errorMessage = respBody?.message || respBody?.error || respBody?.detail || respText;
|
let errorMessage = respBody?.message || respBody?.error || respBody?.detail || respText;
|
||||||
|
|
||||||
// Tratar códigos de status específicos
|
// Tratar códigos de status especÃÂÂficos
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
errorMessage = "Email ou senha incorretos. Verifique suas credenciais.";
|
errorMessage = "Email ou senha incorretos. Verifique suas credenciais.";
|
||||||
} else if (response.status === 403) {
|
} else if (response.status === 403) {
|
||||||
errorMessage = "Acesso negado. Sua conta pode estar inativa.";
|
errorMessage = "Acesso negado. Sua conta pode estar inativa.";
|
||||||
} else if (response.status === 404) {
|
} else if (response.status === 404) {
|
||||||
errorMessage = "Usuário não encontrado. Verifique o email informado.";
|
errorMessage = "Usuário não encontrado. Verifique o email informado.";
|
||||||
} else if (response.status >= 500) {
|
} else if (response.status >= 500) {
|
||||||
errorMessage = "Erro interno do servidor. Tente novamente em alguns minutos.";
|
errorMessage = "Erro interno do servidor. Tente novamente em alguns minutos.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplicar tradução se disponível
|
// Aplicar tradução se disponÃÂÂvel
|
||||||
const translatedError = translateError(errorMessage);
|
const translatedError = translateError(errorMessage);
|
||||||
throw new Error(translatedError);
|
throw new Error(translatedError);
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +145,6 @@ const LoginPageContent = () => {
|
||||||
if (loginData.access_token) {
|
if (loginData.access_token) {
|
||||||
const token = loginData.access_token;
|
const token = loginData.access_token;
|
||||||
localStorage.setItem('access_token', token);
|
localStorage.setItem('access_token', token);
|
||||||
setAccessToken(token);
|
|
||||||
|
|
||||||
// 3. Verificar imediatamente se o /me funciona com o token
|
// 3. Verificar imediatamente se o /me funciona com o token
|
||||||
const meResponse = await fetch(`${baseUrl}/auth/me`, {
|
const meResponse = await fetch(`${baseUrl}/auth/me`, {
|
||||||
|
|
@ -164,24 +159,24 @@ const LoginPageContent = () => {
|
||||||
if (meResponse.ok) {
|
if (meResponse.ok) {
|
||||||
const userData = await meResponse.json();
|
const userData = await meResponse.json();
|
||||||
} else {
|
} else {
|
||||||
console.log("❌ Falha no /me:", await meResponse.text());
|
console.log("âÂÂÅ’ Falha no /me:", await meResponse.text());
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Token não recebido do servidor");
|
throw new Error("Token não recebido do servidor");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Armazenar informações do usuário no localStorage
|
// 4. Armazenar informações do usuário no localStorage
|
||||||
if (loginData.user) {
|
if (loginData.user) {
|
||||||
localStorage.setItem('user', JSON.stringify(loginData.user));
|
localStorage.setItem('user', JSON.stringify(loginData.user));
|
||||||
|
|
||||||
// Verificar se o registro está completo
|
// Verificar se o registro está completo
|
||||||
if (loginData.user["registro-completo"] === false) {
|
if (loginData.user["registro-completo"] === false) {
|
||||||
setShowPendingModal(true);
|
setShowPendingModal(true);
|
||||||
return; // Não continuar com o redirecionamento
|
return; // Não continuar com o redirecionamento
|
||||||
}
|
}
|
||||||
|
|
||||||
// Armazenar ID do endereço se disponível
|
// Armazenar ID do endereço se disponÃÂÂvel
|
||||||
// Backend pode retornar "endereco" (singular) ou "enderecos" (plural array)
|
// Backend pode retornar "endereco" (singular) ou "enderecos" (plural array)
|
||||||
let enderecoId = loginData.user.endereco;
|
let enderecoId = loginData.user.endereco;
|
||||||
|
|
||||||
|
|
@ -195,7 +190,7 @@ const LoginPageContent = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Verificar e armazenar empresa ID se disponível
|
// 5. Verificar e armazenar empresa ID se disponÃÂÂvel
|
||||||
if (loginData.user?.empresaId) {
|
if (loginData.user?.empresaId) {
|
||||||
setEmpresaId(loginData.user.empresaId);
|
setEmpresaId(loginData.user.empresaId);
|
||||||
}
|
}
|
||||||
|
|
@ -204,16 +199,16 @@ const LoginPageContent = () => {
|
||||||
router.push("/dashboard");
|
router.push("/dashboard");
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("❌ Erro no login:", error);
|
console.error("âÂÂÅ’ Erro no login:", error);
|
||||||
|
|
||||||
// Definir mensagem de erro amigável
|
// Definir mensagem de erro amigável
|
||||||
let friendlyMessage = error.message;
|
let friendlyMessage = error.message;
|
||||||
|
|
||||||
// Se for um erro de rede
|
// Se for um erro de rede
|
||||||
if (error.name === 'TypeError' || error.message.includes('fetch')) {
|
if (error.name === 'TypeError' || error.message.includes('fetch')) {
|
||||||
friendlyMessage = "Erro de conexão. Verifique sua internet e tente novamente.";
|
friendlyMessage = "Erro de conexão. Verifique sua internet e tente novamente.";
|
||||||
}
|
}
|
||||||
// Se for um erro genérico sem mensagem clara
|
// Se for um erro genérico sem mensagem clara
|
||||||
else if (!error.message || error.message.length < 5) {
|
else if (!error.message || error.message.length < 5) {
|
||||||
friendlyMessage = "Erro inesperado. Verifique suas credenciais e tente novamente.";
|
friendlyMessage = "Erro inesperado. Verifique suas credenciais e tente novamente.";
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +220,7 @@ const LoginPageContent = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Função para registrar novo usuário usando BFF
|
* Função para registrar novo usuário usando API
|
||||||
* Cria conta e mostra mensagem de sucesso
|
* Cria conta e mostra mensagem de sucesso
|
||||||
*/
|
*/
|
||||||
const register = async () => {
|
const register = async () => {
|
||||||
|
|
@ -233,21 +228,20 @@ const LoginPageContent = () => {
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// 1. Fazer registro no BFF com dados corretos
|
// 1. Fazer registro na API com dados corretos
|
||||||
const baseUrl = BFF_BASE_URL;
|
const baseUrl = API_BASE_URL;
|
||||||
const response = await fetch(`${baseUrl}/auth/register`, {
|
const response = await fetch(baseUrl + '/auth/register', {
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
headers: {
|
||||||
'accept': 'application/json',
|
'accept': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
identificador: email, // identificador = email do usuário (usado no login)
|
identificador: email, // identificador = email do usuário (usado no login)
|
||||||
email: email,
|
email: email,
|
||||||
nome: name, // nome = nome completo do usuário
|
nome: name, // nome = nome completo do usuário
|
||||||
senha: password,
|
senha: password,
|
||||||
nivel: "admin", // valor estático
|
nivel: "owner", // valor estático
|
||||||
"registro-completo": false // valor estático - registro incompleto por padrão
|
"registro-completo": false // valor estático - registro incompleto por padrão
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -255,25 +249,25 @@ const LoginPageContent = () => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
|
||||||
// Tratar códigos de status específicos
|
// Tratar códigos de status especÃÂÂficos
|
||||||
let errorMessage = errorData.message || 'Falha no registro';
|
let errorMessage = errorData.message || 'Falha no registro';
|
||||||
|
|
||||||
if (response.status === 409 || errorMessage.includes('already exists') || errorMessage.includes('já existe')) {
|
if (response.status === 409 || errorMessage.includes('already exists') || errorMessage.includes('já existe')) {
|
||||||
errorMessage = 'Este email já está cadastrado. Tente fazer login ou use outro email.';
|
errorMessage = 'Este email já está cadastrado. Tente fazer login ou use outro email.';
|
||||||
} else if (response.status === 400) {
|
} else if (response.status === 400) {
|
||||||
errorMessage = 'Dados inválidos. Verifique se todos os campos estão preenchidos corretamente.';
|
errorMessage = 'Dados inválidos. Verifique se todos os campos estão preenchidos corretamente.';
|
||||||
} else if (response.status >= 500) {
|
} else if (response.status >= 500) {
|
||||||
errorMessage = 'Erro interno do servidor. Tente novamente em alguns minutos.';
|
errorMessage = 'Erro interno do servidor. Tente novamente em alguns minutos.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplicar tradução se disponível
|
// Aplicar tradução se disponÃÂÂvel
|
||||||
const translatedError = translateError(errorMessage);
|
const translatedError = translateError(errorMessage);
|
||||||
throw new Error(translatedError);
|
throw new Error(translatedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
const registerData = await response.json();
|
const registerData = await response.json();
|
||||||
|
|
||||||
// 2. Fazer login automático após registro para obter token
|
// 2. Fazer login automático após registro para obter token
|
||||||
const loginResponse = await fetch(`${baseUrl}/auth/login`, {
|
const loginResponse = await fetch(`${baseUrl}/auth/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -289,13 +283,13 @@ const LoginPageContent = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!loginResponse.ok) {
|
if (!loginResponse.ok) {
|
||||||
console.error("❌ Erro no login automático:", loginResponse.status);
|
console.error("âÂÂÅ’ Erro no login automático:", loginResponse.status);
|
||||||
throw new Error('Erro ao fazer login automático após registro');
|
throw new Error('Erro ao fazer login automático após registro');
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginData = await loginResponse.json();
|
const loginData = await loginResponse.json();
|
||||||
|
|
||||||
// 3. Armazenar token e dados do usuário
|
// 3. Armazenar token e dados do usuário
|
||||||
if (loginData.access_token) {
|
if (loginData.access_token) {
|
||||||
localStorage.setItem('access_token', loginData.access_token);
|
localStorage.setItem('access_token', loginData.access_token);
|
||||||
}
|
}
|
||||||
|
|
@ -304,20 +298,20 @@ const LoginPageContent = () => {
|
||||||
localStorage.setItem('user', JSON.stringify(loginData.user));
|
localStorage.setItem('user', JSON.stringify(loginData.user));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Redirecionar para completar registro com token válido
|
// 4. Redirecionar para completar registro com token válido
|
||||||
router.push(`/completar-registro?nome=${encodeURIComponent(name)}&email=${encodeURIComponent(email)}`);
|
router.push(`/completar-registro?nome=${encodeURIComponent(name)}&email=${encodeURIComponent(email)}`);
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("❌ Erro no registro:", error);
|
console.error("âÂÂÅ’ Erro no registro:", error);
|
||||||
|
|
||||||
// Definir mensagem de erro amigável
|
// Definir mensagem de erro amigável
|
||||||
let friendlyMessage = error.message;
|
let friendlyMessage = error.message;
|
||||||
|
|
||||||
// Se for um erro de rede
|
// Se for um erro de rede
|
||||||
if (error.name === 'TypeError' || error.message.includes('fetch')) {
|
if (error.name === 'TypeError' || error.message.includes('fetch')) {
|
||||||
friendlyMessage = "Erro de conexão. Verifique sua internet e tente novamente.";
|
friendlyMessage = "Erro de conexão. Verifique sua internet e tente novamente.";
|
||||||
}
|
}
|
||||||
// Se for um erro genérico sem mensagem clara
|
// Se for um erro genérico sem mensagem clara
|
||||||
else if (!error.message || error.message.length < 5) {
|
else if (!error.message || error.message.length < 5) {
|
||||||
friendlyMessage = "Erro inesperado durante o cadastro. Tente novamente.";
|
friendlyMessage = "Erro inesperado durante o cadastro. Tente novamente.";
|
||||||
}
|
}
|
||||||
|
|
@ -329,11 +323,11 @@ const LoginPageContent = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Função para notificar administrador sobre novo cadastro
|
* Função para notificar administrador sobre novo cadastro
|
||||||
*/
|
*/
|
||||||
const notifyAdmin = async (userData: { nome: string, email: string, identificador: string }) => {
|
const notifyAdmin = async (userData: { nome: string, email: string, identificador: string }) => {
|
||||||
try {
|
try {
|
||||||
// Enviar notificações via API (usando Resend para emails reais)
|
// Enviar notificações via API (usando Resend para emails reais)
|
||||||
await fetch('/api/notify-admin-resend', {
|
await fetch('/api/notify-admin-resend', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -342,13 +336,13 @@ const LoginPageContent = () => {
|
||||||
body: JSON.stringify(userData)
|
body: JSON.stringify(userData)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao enviar notificações:", error);
|
console.error("Erro ao enviar notificações:", error);
|
||||||
// Não bloquear o cadastro por erro de notificação
|
// Não bloquear o cadastro por erro de notificação
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Função para fechar modal de sucesso e redirecionar
|
* Função para fechar modal de sucesso e redirecionar
|
||||||
*/
|
*/
|
||||||
const handleSuccessModalClose = () => {
|
const handleSuccessModalClose = () => {
|
||||||
setShowSuccessModal(false);
|
setShowSuccessModal(false);
|
||||||
|
|
@ -356,25 +350,25 @@ const LoginPageContent = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Função para fechar modal de cadastro pendente
|
* Função para fechar modal de cadastro pendente
|
||||||
*/
|
*/
|
||||||
const handlePendingModalClose = () => {
|
const handlePendingModalClose = () => {
|
||||||
setShowPendingModal(false);
|
setShowPendingModal(false);
|
||||||
// Fazer logout para limpar dados do usuário
|
// Fazer logout para limpar dados do usuário
|
||||||
localStorage.removeItem('access_token');
|
localStorage.removeItem('access_token');
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
setAccessToken("");
|
setAccessToken("");
|
||||||
// Redirecionar para página inicial
|
// Redirecionar para página inicial
|
||||||
router.push('/');
|
router.push('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tela de carregamento durante verificação de autenticação
|
// Tela de carregamento durante verificação de autenticação
|
||||||
if (checkingAuth) {
|
if (checkingAuth) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="w-16 h-16 border-4 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
<div className="w-16 h-16 border-4 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
||||||
<p className="text-gray-600">Verificando autenticação...</p>
|
<p className="text-gray-600">Verificando autenticação...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -383,7 +377,7 @@ const LoginPageContent = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||||
<div className="bg-white rounded-2xl shadow-xl overflow-hidden w-full max-w-md">
|
<div className="bg-white rounded-2xl shadow-xl overflow-hidden w-full max-w-md">
|
||||||
{/* Cabeçalho com logo e slogan */}
|
{/* Cabeçalho com logo e slogan */}
|
||||||
<div className="bg-gradient-to-r from-blue-600 to-blue-700 p-1 text-center">
|
<div className="bg-gradient-to-r from-blue-600 to-blue-700 p-1 text-center">
|
||||||
<div className="mx-auto -mb-2 flex items-center justify-center">
|
<div className="mx-auto -mb-2 flex items-center justify-center">
|
||||||
<Image
|
<Image
|
||||||
|
|
@ -399,9 +393,9 @@ const LoginPageContent = () => {
|
||||||
<p className="text-gray-300">Plataforma B2B de Medicamentos</p>
|
<p className="text-gray-300">Plataforma B2B de Medicamentos</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Formulário de login/registro */}
|
{/* Formulário de login/registro */}
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{/* Botões de alternância entre Login e Cadastro */}
|
{/* Botões de alternância entre Login e Cadastro */}
|
||||||
<div className="flex bg-gray-100 rounded-lg p-1 mb-6">
|
<div className="flex bg-gray-100 rounded-lg p-1 mb-6">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsLogin(true)}
|
onClick={() => setIsLogin(true)}
|
||||||
|
|
@ -471,7 +465,7 @@ const LoginPageContent = () => {
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="João Silva"
|
placeholder="João Silva"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors text-gray-900 placeholder-gray-500"
|
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors text-gray-900 placeholder-gray-500"
|
||||||
|
|
@ -534,7 +528,7 @@ const LoginPageContent = () => {
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="w-full pl-10 pr-12 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors text-gray-900 placeholder-gray-500"
|
className="w-full pl-10 pr-12 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors text-gray-900 placeholder-gray-500"
|
||||||
|
|
@ -583,7 +577,7 @@ const LoginPageContent = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Botões de Submit */}
|
{/* Botões de Submit */}
|
||||||
<div className="space-y-3 pt-2">
|
<div className="space-y-3 pt-2">
|
||||||
{isLogin ? (
|
{isLogin ? (
|
||||||
<button
|
<button
|
||||||
|
|
@ -715,18 +709,18 @@ const LoginPageContent = () => {
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Conteúdo do Modal */}
|
{/* Conteúdo do Modal */}
|
||||||
<div className="p-6 text-center">
|
<div className="p-6 text-center">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<p className="text-gray-700 text-lg leading-relaxed">
|
<p className="text-gray-700 text-lg leading-relaxed">
|
||||||
Seu cadastro foi enviado com <strong>sucesso</strong>!
|
Seu cadastro foi enviado com <strong>sucesso</strong>!
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-600 mt-3">
|
<p className="text-gray-600 mt-3">
|
||||||
Nossa equipe entrará em contato em breve para finalizar seu acesso à plataforma.
|
Nossa equipe entrará em contato em breve para finalizar seu acesso àplataforma.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Ícone decorativo */}
|
{/* ÃÂÂcone decorativo */}
|
||||||
<div className="flex justify-center mb-6">
|
<div className="flex justify-center mb-6">
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<div className="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
|
<div className="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
|
||||||
|
|
@ -735,7 +729,7 @@ const LoginPageContent = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Botão de OK */}
|
{/* Botão de OK */}
|
||||||
<button
|
<button
|
||||||
onClick={handleSuccessModalClose}
|
onClick={handleSuccessModalClose}
|
||||||
className="w-full bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-green-200"
|
className="w-full bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-green-200"
|
||||||
|
|
@ -773,21 +767,21 @@ const LoginPageContent = () => {
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Conteúdo do Modal */}
|
{/* Conteúdo do Modal */}
|
||||||
<div className="p-6 text-center">
|
<div className="p-6 text-center">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<p className="text-gray-700 text-lg leading-relaxed">
|
<p className="text-gray-700 text-lg leading-relaxed">
|
||||||
Seu cadastro está <strong>pendente de validação</strong>!
|
Seu cadastro está <strong>pendente de validação</strong>!
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-600 mt-3">
|
<p className="text-gray-600 mt-3">
|
||||||
Nossa equipe ainda está analisando suas informações. Você receberá uma confirmação em breve.
|
Nossa equipe ainda está analisando suas informações. Você receberá uma confirmação em breve.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-600 mt-3">
|
<p className="text-gray-600 mt-3">
|
||||||
Qualquer dúvida, entre em contato conosco.
|
Qualquer dúvida, entre em contato conosco.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Ícone decorativo */}
|
{/* ÃÂÂcone decorativo */}
|
||||||
<div className="flex justify-center mb-6">
|
<div className="flex justify-center mb-6">
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<div className="w-3 h-3 bg-yellow-500 rounded-full animate-pulse"></div>
|
<div className="w-3 h-3 bg-yellow-500 rounded-full animate-pulse"></div>
|
||||||
|
|
@ -796,7 +790,7 @@ const LoginPageContent = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Botão de OK */}
|
{/* Botão de OK */}
|
||||||
<button
|
<button
|
||||||
onClick={handlePendingModalClose}
|
onClick={handlePendingModalClose}
|
||||||
className="w-full bg-gradient-to-r from-yellow-500 to-orange-500 hover:from-yellow-600 hover:to-orange-600 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-yellow-200"
|
className="w-full bg-gradient-to-r from-yellow-500 to-orange-500 hover:from-yellow-600 hover:to-orange-600 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-yellow-200"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
// @ts-nocheck
|
import React, { useState } from 'react';
|
||||||
import React from 'react';
|
|
||||||
import { LaboratorioBff } from '@/services/laboratorioApiService';
|
import { LaboratorioBff } from '@/services/laboratorioApiService';
|
||||||
import SearchBar from './SearchBar';
|
|
||||||
import DataTable, { Column } from './DataTable';
|
|
||||||
import Pagination from './Pagination';
|
|
||||||
import TableActions from './TableActions';
|
|
||||||
|
|
||||||
interface LaboratorioListProps {
|
interface LaboratorioListProps {
|
||||||
laboratorios: LaboratorioBff[];
|
laboratorios: LaboratorioBff[];
|
||||||
|
|
@ -37,13 +32,12 @@ const LaboratorioList: React.FC<LaboratorioListProps> = ({
|
||||||
onPrevPage,
|
onPrevPage,
|
||||||
onNextPage,
|
onNextPage,
|
||||||
onSearch,
|
onSearch,
|
||||||
onRefresh
|
onRefresh,
|
||||||
}) => {
|
}) => {
|
||||||
const totalPages = Math.ceil(totalLaboratorios / pageSize);
|
const totalPages = Math.ceil(totalLaboratorios / pageSize);
|
||||||
const startItem = (currentPage - 1) * pageSize + 1;
|
const startItem = (currentPage - 1) * pageSize + 1;
|
||||||
const endItem = Math.min(currentPage * pageSize, totalLaboratorios);
|
const endItem = Math.min(currentPage * pageSize, totalLaboratorios);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
const [search, setSearch] = React.useState('');
|
|
||||||
|
|
||||||
const handleSearch = (e: React.FormEvent) => {
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -51,20 +45,15 @@ const LaboratorioList: React.FC<LaboratorioListProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
if (confirm('Tem certeza que deseja deletar este laboratório?')) {
|
if (confirm('Tem certeza que deseja deletar este laboratório?')) {
|
||||||
await onDelete(id);
|
await onDelete(id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: Column<LaboratorioBff>[] = [
|
|
||||||
{ key: 'nome', header: 'Nome' },
|
|
||||||
{ key: 'createdAt', header: 'Data de Criação', render: row => row.createdAt ? new Date(row.createdAt).toLocaleDateString('pt-BR') : '-' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h1 className="text-3xl font-bold">Lista de Laboratórios</h1>
|
<h1 className="text-3xl font-bold">Lista de Laboratórios</h1>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<form onSubmit={handleSearch} className="flex gap-2">
|
<form onSubmit={handleSearch} className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
|
|
@ -77,17 +66,17 @@ const LaboratorioList: React.FC<LaboratorioListProps> = ({
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors cursor-pointer"
|
className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors cursor-pointer"
|
||||||
disabled={loading}
|
disabled={loading || isChangingPage}
|
||||||
>
|
>
|
||||||
🔠Buscar
|
Buscar
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<button
|
<button
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
disabled={loading}
|
disabled={loading || isChangingPage}
|
||||||
className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 transition-colors cursor-pointer"
|
className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
{loading ? 'â³' : '🔄'} Atualizar
|
{loading ? 'Processando...' : 'Atualizar'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -102,14 +91,14 @@ const LaboratorioList: React.FC<LaboratorioListProps> = ({
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-md p-3 mb-4">
|
<div className="bg-blue-50 border border-blue-200 rounded-md p-3 mb-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
|
||||||
<span className="text-blue-800 text-sm">Criando novo laboratório...</span>
|
<span className="text-blue-800 text-sm">Criando novo laboratório...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex justify-center items-center min-h-screen">
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
<div className="text-lg">Carregando laboratórios...</div>
|
<div className="text-lg">Carregando laboratórios...</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
@ -117,51 +106,39 @@ const LaboratorioList: React.FC<LaboratorioListProps> = ({
|
||||||
<table className="min-w-full table-auto">
|
<table className="min-w-full table-auto">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nome</th>
|
||||||
Nome
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||||
</th>
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Data de Criação</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ações</th>
|
||||||
ID
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Data de Criação
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Ações
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{laboratorios.length === 0 ? (
|
{laboratorios.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4} className="px-6 py-4 text-center text-gray-500">
|
<td colSpan={4} className="px-6 py-4 text-center text-gray-500">
|
||||||
📠Nenhum laboratório encontrado.
|
Nenhum laboratório encontrado.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
laboratorios.map((lab) => (
|
laboratorios.map((lab) => (
|
||||||
<tr key={lab.$id}>
|
<tr key={lab.id}>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{lab.nome || 'N/A'}</td>
|
||||||
{lab.nome || 'N/A'}
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{lab.id}</td>
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
{lab.$id}
|
{lab.createdAt ? new Date(lab.createdAt).toLocaleDateString('pt-BR') : '-'}
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
||||||
{new Date(lab.$createdAt).toLocaleDateString('pt-BR')}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
onClick={() => onEdit(lab)}
|
onClick={() => onEdit(lab)}
|
||||||
className="px-3 py-1 text-sm bg-yellow-100 text-yellow-800 rounded hover:bg-yellow-200 transition-colors cursor-pointer mr-2"
|
className="px-3 py-1 text-sm bg-yellow-100 text-yellow-800 rounded hover:bg-yellow-200 transition-colors cursor-pointer mr-2"
|
||||||
>
|
>
|
||||||
âœï¸ Editar
|
Editar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(lab.$id)}
|
onClick={() => handleDelete(lab.id)}
|
||||||
className="px-3 py-1 text-sm bg-red-100 text-red-800 rounded hover:bg-red-200 transition-colors cursor-pointer"
|
className="px-3 py-1 text-sm bg-red-100 text-red-800 rounded hover:bg-red-200 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
ðŸ—‘ï¸ Deletar
|
Deletar
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -171,12 +148,12 @@ const LaboratorioList: React.FC<LaboratorioListProps> = ({
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pagination-container">
|
<div className="mt-4 flex justify-between items-center">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
{totalLaboratorios > 0 ? (
|
{totalLaboratorios > 0 ? (
|
||||||
<>Mostrando {startItem} - {endItem} de {totalLaboratorios} laboratórios</>
|
<>Mostrando {startItem} - {endItem} de {totalLaboratorios} laboratórios</>
|
||||||
) : (
|
) : (
|
||||||
'Total de laboratórios: 0'
|
'Total de laboratórios: 0'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -184,20 +161,18 @@ const LaboratorioList: React.FC<LaboratorioListProps> = ({
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={onPrevPage}
|
onClick={onPrevPage}
|
||||||
disabled={currentPage <= 1}
|
disabled={currentPage <= 1 || isChangingPage}
|
||||||
className="px-3 py-1 border rounded disabled:opacity-50 cursor-pointer"
|
className="px-3 py-1 border rounded disabled:opacity-50 cursor-pointer"
|
||||||
>
|
>
|
||||||
â—€ï¸ Anterior
|
Anterior
|
||||||
</button>
|
</button>
|
||||||
<span className="px-3 py-1 text-gray-700">
|
<span className="px-3 py-1 text-gray-700">Página {currentPage} de {totalPages}</span>
|
||||||
Página {currentPage} de {totalPages}
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
onClick={onNextPage}
|
onClick={onNextPage}
|
||||||
disabled={currentPage >= totalPages}
|
disabled={currentPage >= totalPages || isChangingPage}
|
||||||
className="px-3 py-1 border rounded disabled:opacity-50 cursor-pointer"
|
className="px-3 py-1 border rounded disabled:opacity-50 cursor-pointer"
|
||||||
>
|
>
|
||||||
Próxima â–¶ï¸
|
Próxima
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -209,4 +184,3 @@ const LaboratorioList: React.FC<LaboratorioListProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LaboratorioList;
|
export default LaboratorioList;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// @ts-nocheck
|
import React, { useEffect, useState } from 'react';
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Models } from 'appwrite';
|
import { Models } from 'appwrite';
|
||||||
import { UsuarioFormData } from '@/hooks/useUsuarios';
|
import { UsuarioFormData } from '@/hooks/useUsuarios';
|
||||||
|
|
||||||
|
|
@ -10,102 +9,115 @@ interface UsuarioFormProps {
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UsuarioDocument = Models.Document & UsuarioFormData;
|
||||||
|
|
||||||
|
const buildEmptyUsuario = (): UsuarioFormData => ({
|
||||||
|
'nome-civil': '',
|
||||||
|
'nome-social': '',
|
||||||
|
cpf: '',
|
||||||
|
'auth-id-appwrite': '',
|
||||||
|
enderecos: [],
|
||||||
|
empresas: [],
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
|
||||||
const UsuarioForm: React.FC<UsuarioFormProps> = ({
|
const UsuarioForm: React.FC<UsuarioFormProps> = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
initialData,
|
initialData,
|
||||||
loading = false,
|
loading = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [formData, setFormData] = useState<UsuarioFormData>(buildEmptyUsuario);
|
||||||
|
|
||||||
const [formData, setFormData] = useState<UsuarioFormData>({
|
|
||||||
'nome-civil': '',
|
|
||||||
'nome-social': '',
|
|
||||||
cpf: '',
|
|
||||||
'auth-id-appwrite': '',
|
|
||||||
enderecos: [],
|
|
||||||
empresas: [],
|
|
||||||
email: '',
|
|
||||||
});
|
|
||||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (!initialData) {
|
||||||
setFormData({
|
setFormData(buildEmptyUsuario());
|
||||||
'nome-civil': initialData['nome-civil'] || '',
|
return;
|
||||||
'nome-social': initialData['nome-social'] || '',
|
|
||||||
cpf: initialData.cpf || '',
|
|
||||||
'auth-id-appwrite': initialData['auth-id-appwrite'] || '',
|
|
||||||
enderecos: initialData.enderecos || [],
|
|
||||||
empresas: initialData.empresas || [],
|
|
||||||
email: initialData.email || '',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setFormData({
|
|
||||||
'nome-civil': '',
|
|
||||||
'nome-social': '',
|
|
||||||
cpf: '',
|
|
||||||
'auth-id-appwrite': '',
|
|
||||||
enderecos: [],
|
|
||||||
empresas: [],
|
|
||||||
email: '',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const document = initialData as UsuarioDocument;
|
||||||
|
setFormData({
|
||||||
|
'nome-civil': document['nome-civil'] || '',
|
||||||
|
'nome-social': document['nome-social'] || '',
|
||||||
|
cpf: document.cpf || '',
|
||||||
|
'auth-id-appwrite': document['auth-id-appwrite'] || '',
|
||||||
|
enderecos: document.enderecos || [],
|
||||||
|
empresas: document.empresas || [],
|
||||||
|
email: document.email || '',
|
||||||
|
});
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const formatCpf = (value: string) => {
|
||||||
e.preventDefault();
|
const digits = value.replace(/\D/g, '').slice(0, 11);
|
||||||
|
|
||||||
const payload = { ...formData, cpf: formData.cpf.replace(/\D/g, '') };
|
|
||||||
const success = await onSubmit(payload);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
setMessage({
|
|
||||||
type: 'success',
|
|
||||||
text: initialData ? '🔄 Usuário atualizado com sucesso!' : '🎉 Usuário cadastrado com sucesso!',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!initialData) {
|
|
||||||
setFormData({
|
|
||||||
'nome-civil': '',
|
|
||||||
'nome-social': '',
|
|
||||||
cpf: '',
|
|
||||||
'auth-id-appwrite': '',
|
|
||||||
enderecos: [],
|
|
||||||
empresas: [],
|
|
||||||
email: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => setMessage(null), 3000);
|
|
||||||
} else {
|
|
||||||
setMessage({
|
|
||||||
type: 'error',
|
|
||||||
text: initialData ? 'Erro ao atualizar usuário' : 'Erro ao cadastrar usuário',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCpf = (val: string) => {
|
|
||||||
const digits = val.replace(/\D/g, '').slice(0, 11);
|
|
||||||
return digits
|
return digits
|
||||||
.replace(/(\d{3})(\d)/, '$1.$2')
|
.replace(/(\d{3})(\d)/, '$1.$2')
|
||||||
.replace(/(\d{3})(\d)/, '$1.$2')
|
.replace(/(\d{3})(\d)/, '$1.$2')
|
||||||
.replace(/(\d{3})(\d{1,2})$/, '$1-$2');
|
.replace(/(\d{3})(\d{1,2})$/, '$1-$2');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseList = (value: string) =>
|
||||||
|
value
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const payload: UsuarioFormData = {
|
||||||
|
...formData,
|
||||||
|
cpf: formData.cpf.replace(/\D/g, ''),
|
||||||
|
enderecos: [...(formData.enderecos || [])],
|
||||||
|
empresas: [...(formData.empresas || [])],
|
||||||
|
};
|
||||||
|
|
||||||
|
const success = await onSubmit(payload);
|
||||||
|
if (success) {
|
||||||
|
setMessage({
|
||||||
|
type: 'success',
|
||||||
|
text: initialData ? 'Usuário atualizado com sucesso!' : 'Usuário cadastrado com sucesso!',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!initialData) {
|
||||||
|
setFormData(buildEmptyUsuario());
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => setMessage(null), 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessage({
|
||||||
|
type: 'error',
|
||||||
|
text: initialData ? 'Erro ao atualizar usuário' : 'Erro ao cadastrar usuário',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
const val = name === 'cpf' ? formatCpf(value) : value;
|
|
||||||
setFormData(prev => ({ ...prev, [name]: val }));
|
setFormData((prev) => {
|
||||||
if (message) setMessage(null);
|
if (name === 'cpf') {
|
||||||
|
return { ...prev, cpf: formatCpf(value) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'enderecos' || name === 'empresas') {
|
||||||
|
return { ...prev, [name]: parseList(value) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...prev, [name]: value };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
setMessage(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||||
{initialData ? 'âœï¸ Editar Usuário' : ' Novo Usuário'}
|
{initialData ? 'Editar Usuário' : 'Novo Usuário'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
{initialData ? 'Atualize os dados do usuário.' : 'Cadastre um novo usuário na plataforma SaveInMed.'}
|
{initialData ? 'Atualize os dados do usuário.' : 'Cadastre um novo usuário na plataforma SaveInMed.'}
|
||||||
|
|
@ -113,8 +125,11 @@ const UsuarioForm: React.FC<UsuarioFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{message && (
|
{message && (
|
||||||
<div className={`mb-4 p-4 rounded-md ${
|
<div
|
||||||
message.type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-700 border border-red-200'
|
className={`mb-4 p-4 rounded-md ${
|
||||||
|
message.type === 'success'
|
||||||
|
? 'bg-green-50 text-green-700 border border-green-200'
|
||||||
|
: 'bg-red-50 text-red-700 border border-red-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
|
|
@ -166,7 +181,7 @@ const UsuarioForm: React.FC<UsuarioFormProps> = ({
|
||||||
value={formData.cpf}
|
value={formData.cpf}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
required
|
required
|
||||||
maxLength={11}
|
maxLength={14}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed"
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||||
placeholder="Digite o CPF"
|
placeholder="Digite o CPF"
|
||||||
|
|
@ -190,34 +205,34 @@ const UsuarioForm: React.FC<UsuarioFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="enderecos" className="block text-sm font-medium text-gray-700 mb-2">
|
<label htmlFor="enderecos" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Endereço (ID) *
|
Endereços (IDs separados por vÃrgula) *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="enderecos"
|
id="enderecos"
|
||||||
name="enderecos"
|
name="enderecos"
|
||||||
value={formData.enderecos}
|
value={(formData.enderecos || []).join(', ')}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
required
|
required
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed"
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||||
placeholder="ID do endereço"
|
placeholder="id-1, id-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="empresas" className="block text-sm font-medium text-gray-700 mb-2">
|
<label htmlFor="empresas" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Empresa (ID) *
|
Empresas (IDs separados por vÃrgula) *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="empresas"
|
id="empresas"
|
||||||
name="empresas"
|
name="empresas"
|
||||||
value={formData.empresas}
|
value={(formData.empresas || []).join(', ')}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
required
|
required
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed"
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||||
placeholder="ID da empresa"
|
placeholder="id-empresa-1, id-empresa-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -244,7 +259,7 @@ const UsuarioForm: React.FC<UsuarioFormProps> = ({
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors cursor-pointer"
|
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
{loading ? 'ⳠProcessando...' : initialData ? '🔄 Atualizar' : '➕ Cadastrar'}
|
{loading ? 'Processando...' : initialData ? 'Atualizar' : 'Cadastrar'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{onCancel && (
|
{onCancel && (
|
||||||
|
|
@ -254,7 +269,7 @@ const UsuarioForm: React.FC<UsuarioFormProps> = ({
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex-1 bg-gray-600 text-white py-2 px-4 rounded-md hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors cursor-pointer"
|
className="flex-1 bg-gray-600 text-white py-2 px-4 rounded-md hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
⌠Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -264,4 +279,3 @@ const UsuarioForm: React.FC<UsuarioFormProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UsuarioForm;
|
export default UsuarioForm;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
// SERVIÇO DESABILITADO - MIGRADO PARA BFF
|
// SERVIÇO DESABILITADO - MIGRADO PARA BFF
|
||||||
// Este serviço não é mais usado após migração para BFF
|
// Este serviço não é mais usado após migração para BFF
|
||||||
|
|
||||||
export interface UsuarioData {
|
export interface UsuarioData {
|
||||||
'nome-civil': string;
|
'nome-civil': string;
|
||||||
'nome-social': string;
|
'nome-social': string;
|
||||||
cpf: string;
|
cpf: string;
|
||||||
|
'auth-id-appwrite'?: string;
|
||||||
email: string;
|
email: string;
|
||||||
enderecos?: string[];
|
enderecos?: string[];
|
||||||
empresas?: string[];
|
empresas?: string[];
|
||||||
|
|
@ -14,6 +15,7 @@ export interface UsuarioUpdateData {
|
||||||
'nome-civil'?: string;
|
'nome-civil'?: string;
|
||||||
'nome-social'?: string;
|
'nome-social'?: string;
|
||||||
cpf?: string;
|
cpf?: string;
|
||||||
|
'auth-id-appwrite'?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
enderecos?: string[];
|
enderecos?: string[];
|
||||||
empresas?: string[];
|
empresas?: string[];
|
||||||
|
|
@ -31,7 +33,7 @@ export interface PaginatedResponse<T> extends ApiResponse<T> {
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Funções desabilitadas - retornam dados vazios
|
// Funções desabilitadas - retornam dados vazios
|
||||||
class UsuarioService {
|
class UsuarioService {
|
||||||
async listar(page = 1, limit = 10): Promise<PaginatedResponse<any>> {
|
async listar(page = 1, limit = 10): Promise<PaginatedResponse<any>> {
|
||||||
console.warn('UsuarioService DESABILITADO - use BFF');
|
console.warn('UsuarioService DESABILITADO - use BFF');
|
||||||
|
|
@ -55,7 +57,7 @@ class UsuarioService {
|
||||||
console.warn('UsuarioService DESABILITADO - use BFF');
|
console.warn('UsuarioService DESABILITADO - use BFF');
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Serviço desabilitado - use BFF'
|
error: 'Serviço desabilitado - use BFF'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,7 +65,7 @@ class UsuarioService {
|
||||||
console.warn('UsuarioService DESABILITADO - use BFF');
|
console.warn('UsuarioService DESABILITADO - use BFF');
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Serviço desabilitado - use BFF'
|
error: 'Serviço desabilitado - use BFF'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +73,7 @@ class UsuarioService {
|
||||||
console.warn('UsuarioService DESABILITADO - use BFF');
|
console.warn('UsuarioService DESABILITADO - use BFF');
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Serviço desabilitado - use BFF'
|
error: 'Serviço desabilitado - use BFF'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +81,7 @@ class UsuarioService {
|
||||||
console.warn('UsuarioService DESABILITADO - use BFF');
|
console.warn('UsuarioService DESABILITADO - use BFF');
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Serviço desabilitado - use BFF'
|
error: 'Serviço desabilitado - use BFF'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue