Primeiro commit
This commit is contained in:
79
.env.example
Normal file
79
.env.example
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# =====================================================
|
||||||
|
# CONFIGURAÇÕES DO SUPABASE
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# URL do seu projeto Supabase
|
||||||
|
# Encontre em: https://supabase.com/dashboard/project/SEU_PROJETO/settings/api
|
||||||
|
SUPABASE_URL=https://seu-projeto-id.supabase.co
|
||||||
|
|
||||||
|
# Chave anônima do Supabase (public anon key)
|
||||||
|
# Encontre em: https://supabase.com/dashboard/project/SEU_PROJETO/settings/api
|
||||||
|
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# CONFIGURAÇÕES DO SERVIDOR
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Ambiente de execução (development, production)
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# Porta do servidor (padrão: 5000)
|
||||||
|
PORT=5000
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# CONFIGURAÇÕES OPCIONAIS
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Chave secreta para JWT (se implementado futuramente)
|
||||||
|
# JWT_SECRET=sua_chave_secreta_muito_forte_aqui
|
||||||
|
|
||||||
|
# Configurações de upload (se necessário)
|
||||||
|
# MAX_FILE_SIZE=5242880
|
||||||
|
# UPLOAD_PATH=./uploads
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# INSTRUÇÕES
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# 1. Copie este arquivo para .env
|
||||||
|
# 2. Substitua os valores pelos seus dados reais do Supabase
|
||||||
|
# 3. Nunca commite o arquivo .env no Git
|
||||||
|
# 4. Para produção, use variáveis de ambiente do servidor/plataforma
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# CONFIGURAÇÕES GOOGLE SHEETS (OPCIONAL)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Para usar a integração com Google Sheets:
|
||||||
|
# 1. Acesse https://console.cloud.google.com/
|
||||||
|
# 2. Crie um novo projeto ou selecione um existente
|
||||||
|
# 3. Ative as APIs: Google Sheets API e Google Drive API
|
||||||
|
# 4. Crie credenciais OAuth 2.0 para aplicação web
|
||||||
|
# 5. Baixe o arquivo JSON das credenciais
|
||||||
|
# 6. Renomeie para 'google-credentials.json' e coloque na pasta /config/
|
||||||
|
# 7. Configure o URI de redirecionamento: http://localhost:5000/auth/google/callback
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# CONFIGURAÇÕES PIX - MERCADO PAGO
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Access Token do Mercado Pago (TEST para desenvolvimento, PROD para produção)
|
||||||
|
MERCADOPAGO_ACCESS_TOKEN=TEST-sua_access_token_aqui
|
||||||
|
|
||||||
|
# Public Key do Mercado Pago
|
||||||
|
MERCADOPAGO_PUBLIC_KEY=pk_test_sua_public_key_aqui
|
||||||
|
|
||||||
|
# URL base para webhooks (importante para produção)
|
||||||
|
BASE_URL=http://localhost:5000
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# INSTRUÇÕES PIX
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# 1. Acesse: https://www.mercadopago.com.br/developers
|
||||||
|
# 2. Crie uma conta de desenvolvedor
|
||||||
|
# 3. Vá em "Suas integrações" > "Criar aplicação"
|
||||||
|
# 4. Escolha "Pagamentos online" e "Checkout Pro"
|
||||||
|
# 5. Copie as credenciais de TEST para desenvolvimento
|
||||||
|
# 6. Para produção, use as credenciais PROD
|
||||||
|
# 7. Configure o webhook URL no painel do Mercado Pago
|
||||||
105
.gitignore
vendored
Normal file
105
.gitignore
vendored
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
client/node_modules/
|
||||||
|
|
||||||
|
# Production builds
|
||||||
|
client/build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
public
|
||||||
|
|
||||||
|
# Storybook build outputs
|
||||||
|
.out
|
||||||
|
.storybook-out
|
||||||
|
|
||||||
|
# Temporary folders
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Uploads directory (keep structure but ignore files)
|
||||||
|
uploads/*
|
||||||
|
!uploads/.gitkeep
|
||||||
98
ADICIONAR-CAMPO-DESCRICAO.md
Normal file
98
ADICIONAR-CAMPO-DESCRICAO.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# 📝 Como Adicionar o Campo Descrição na Tabela Produtos
|
||||||
|
|
||||||
|
## ⚠️ **IMPORTANTE: Execute este passo para ativar a funcionalidade de descrição**
|
||||||
|
|
||||||
|
Para que a funcionalidade de descrição de produtos funcione completamente, você precisa adicionar o campo `descricao` na tabela `produtos` do Supabase.
|
||||||
|
|
||||||
|
## 🔧 **Passos para Adicionar o Campo:**
|
||||||
|
|
||||||
|
### **Opção 1: Via Painel do Supabase (Recomendado)**
|
||||||
|
|
||||||
|
1. **Acesse o Painel do Supabase:**
|
||||||
|
- Vá para [https://supabase.com/dashboard](https://supabase.com/dashboard)
|
||||||
|
- Faça login na sua conta
|
||||||
|
- Selecione seu projeto
|
||||||
|
|
||||||
|
2. **Navegue até o Editor SQL:**
|
||||||
|
- No menu lateral, clique em **"SQL Editor"**
|
||||||
|
- Ou vá para **"Table Editor"** > **"produtos"** > **"Add Column"**
|
||||||
|
|
||||||
|
3. **Execute o Comando SQL:**
|
||||||
|
```sql
|
||||||
|
ALTER TABLE produtos ADD COLUMN descricao TEXT;
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Ou Adicione via Interface:**
|
||||||
|
- Na **Table Editor**, selecione a tabela **"produtos"**
|
||||||
|
- Clique em **"Add Column"**
|
||||||
|
- Nome: `descricao`
|
||||||
|
- Tipo: `text`
|
||||||
|
- Nullable: ✅ (permitir valores nulos)
|
||||||
|
- Clique em **"Save"**
|
||||||
|
|
||||||
|
### **Opção 2: Via psql (Se disponível)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Conecte ao seu banco Supabase
|
||||||
|
psql "postgresql://postgres:[SUA_SENHA]@db.[SEU_PROJETO_ID].supabase.co:5432/postgres"
|
||||||
|
|
||||||
|
# Execute o comando
|
||||||
|
ALTER TABLE produtos ADD COLUMN descricao TEXT;
|
||||||
|
|
||||||
|
# Verifique se foi adicionado
|
||||||
|
\d produtos
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ **Verificação**
|
||||||
|
|
||||||
|
Após adicionar o campo, você pode verificar se funcionou:
|
||||||
|
|
||||||
|
1. **Teste a API:**
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:5000/api/produtos"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Teste no Frontend:**
|
||||||
|
- Acesse a página de produtos
|
||||||
|
- Tente cadastrar um novo produto
|
||||||
|
- O campo descrição deve aparecer
|
||||||
|
- O botão "Gerar com IA" deve funcionar
|
||||||
|
|
||||||
|
## 🎯 **Funcionalidades Ativadas Após Adicionar o Campo:**
|
||||||
|
|
||||||
|
- ✅ **Campo Descrição:** Textarea para descrição detalhada
|
||||||
|
- ✅ **Geração com IA:** Botão para gerar descrição automaticamente
|
||||||
|
- ✅ **Salvamento:** Descrição salva junto com o produto
|
||||||
|
- ✅ **Edição:** Possibilidade de editar descrições existentes
|
||||||
|
|
||||||
|
## 🔄 **Status Atual do Sistema:**
|
||||||
|
|
||||||
|
- ✅ **Cálculo de Margem:** Funcionando perfeitamente
|
||||||
|
- ✅ **Configuração ChatGPT:** Implementada nas configurações
|
||||||
|
- ✅ **APIs Backend:** Todas criadas e funcionando
|
||||||
|
- ⚠️ **Campo Descrição:** Requer adição manual no banco
|
||||||
|
- ✅ **Interface:** Pronta para usar assim que o campo for adicionado
|
||||||
|
|
||||||
|
## 🆘 **Se Tiver Problemas:**
|
||||||
|
|
||||||
|
1. **Erro de Permissão:**
|
||||||
|
- Verifique se você tem permissões de administrador no projeto Supabase
|
||||||
|
|
||||||
|
2. **Campo Não Aparece:**
|
||||||
|
- Aguarde alguns minutos para o cache atualizar
|
||||||
|
- Reinicie o servidor Node.js
|
||||||
|
|
||||||
|
3. **Ainda com Erro:**
|
||||||
|
- Verifique os logs do servidor
|
||||||
|
- Teste a API diretamente com curl/Postman
|
||||||
|
|
||||||
|
## 📞 **Suporte:**
|
||||||
|
|
||||||
|
Se precisar de ajuda, verifique:
|
||||||
|
- Console do navegador para erros JavaScript
|
||||||
|
- Logs do servidor Node.js
|
||||||
|
- Painel de logs do Supabase
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Após executar estes passos, todas as funcionalidades de descrição e IA estarão 100% operacionais!** 🚀
|
||||||
233
CHAT-WHATSAPP-GUIDE.md
Normal file
233
CHAT-WHATSAPP-GUIDE.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# 💬 Sistema de Chat WhatsApp - Liberi Kids
|
||||||
|
|
||||||
|
## 🎯 **Funcionalidades Implementadas**
|
||||||
|
|
||||||
|
### **1. Chat Integrado nas Vendas**
|
||||||
|
- **Botão Chat (💬):** Abre conversa completa com histórico
|
||||||
|
- **Botão Mensagem Rápida (📱):** Envia mensagem pré-formatada via WhatsApp Web
|
||||||
|
- **Histórico Persistente:** Todas as mensagens ficam salvas no sistema
|
||||||
|
- **Interface Profissional:** Design similar ao WhatsApp
|
||||||
|
|
||||||
|
### **2. Chat Integrado nos Clientes**
|
||||||
|
- **Botão WhatsApp:** Disponível apenas para clientes com WhatsApp cadastrado
|
||||||
|
- **Acesso Direto:** Chat direto da lista de clientes
|
||||||
|
- **Conversa Personalizada:** Nome do cliente automaticamente identificado
|
||||||
|
|
||||||
|
## 🚀 **Como Usar o Sistema de Chat**
|
||||||
|
|
||||||
|
### **📋 Pré-requisitos**
|
||||||
|
1. **Evolution API Configurada:**
|
||||||
|
- Acesse: Configurações → Evolution API - WhatsApp
|
||||||
|
- Configure: URL, Instância e API Key
|
||||||
|
- Teste a conexão
|
||||||
|
|
||||||
|
2. **Cliente com WhatsApp:**
|
||||||
|
- Cliente deve ter número de WhatsApp cadastrado
|
||||||
|
- Formato aceito: (43) 99976-2754 ou 43999762754
|
||||||
|
|
||||||
|
### **💬 Usando o Chat nas Vendas**
|
||||||
|
|
||||||
|
1. **Acesse:** Vendas → Lista de vendas
|
||||||
|
2. **Localize:** Venda do cliente desejado
|
||||||
|
3. **Clique:** Botão verde com ícone de mensagem (💬)
|
||||||
|
4. **Chat Abre:** Interface completa de conversa
|
||||||
|
5. **Digite:** Sua mensagem na caixa de texto
|
||||||
|
6. **Envie:** Pressione Enter ou clique no botão enviar
|
||||||
|
7. **Histórico:** Todas as mensagens ficam salvas
|
||||||
|
|
||||||
|
### **👥 Usando o Chat nos Clientes**
|
||||||
|
|
||||||
|
1. **Acesse:** Clientes → Lista de clientes
|
||||||
|
2. **Localize:** Cliente com WhatsApp cadastrado
|
||||||
|
3. **Clique:** Botão verde WhatsApp (💬)
|
||||||
|
4. **Converse:** Interface igual ao das vendas
|
||||||
|
5. **Histórico:** Mensagens organizadas por cliente
|
||||||
|
|
||||||
|
## 🎨 **Interface do Chat**
|
||||||
|
|
||||||
|
### **📱 Design Profissional**
|
||||||
|
- **Header:** Nome e telefone do cliente
|
||||||
|
- **Área de Mensagens:** Histórico completo da conversa
|
||||||
|
- **Mensagens Enviadas:** Aparecem à direita (verde)
|
||||||
|
- **Mensagens Recebidas:** Aparecem à esquerda (branco)
|
||||||
|
- **Input:** Caixa de texto para digitar mensagens
|
||||||
|
|
||||||
|
### **⏰ Informações das Mensagens**
|
||||||
|
- **Horário:** Cada mensagem mostra quando foi enviada
|
||||||
|
- **Status:** Indicadores de entrega (✓ ✓)
|
||||||
|
- **Data:** Separador automático por dia
|
||||||
|
- **Nome:** Identificação do remetente
|
||||||
|
|
||||||
|
## 🔧 **Funcionalidades Técnicas**
|
||||||
|
|
||||||
|
### **📡 Integração Evolution API**
|
||||||
|
- **Envio Automático:** Mensagens enviadas via Evolution API
|
||||||
|
- **Fallback:** Se Evolution falhar, abre WhatsApp Web
|
||||||
|
- **Configuração:** Credenciais salvas no sistema
|
||||||
|
- **Teste:** Botão para testar conexão
|
||||||
|
|
||||||
|
### **💾 Armazenamento**
|
||||||
|
- **Banco de Dados:** Histórico salvo no Supabase
|
||||||
|
- **Tabela:** `mensagens_whatsapp`
|
||||||
|
- **Campos:** telefone, mensagem, tipo, status, data
|
||||||
|
- **Backup:** Dados seguros na nuvem
|
||||||
|
|
||||||
|
### **🔄 Estados das Mensagens**
|
||||||
|
- **Enviando:** ⏳ (temporário)
|
||||||
|
- **Enviada:** ✓ (confirmada)
|
||||||
|
- **Entregue:** ✓✓ (futuramente)
|
||||||
|
- **Lida:** ✓✓ (futuramente)
|
||||||
|
|
||||||
|
## 📋 **Estrutura do Sistema**
|
||||||
|
|
||||||
|
### **🗂️ Arquivos Principais**
|
||||||
|
```
|
||||||
|
/client/src/components/
|
||||||
|
├── ChatWhatsApp.js # Componente principal do chat
|
||||||
|
├── ChatWhatsApp.css # Estilos do chat
|
||||||
|
|
||||||
|
/client/src/pages/
|
||||||
|
├── Vendas.js # Chat integrado nas vendas
|
||||||
|
├── Clientes.js # Chat integrado nos clientes
|
||||||
|
|
||||||
|
/server-supabase.js # APIs do chat
|
||||||
|
├── GET /api/chat/:telefone # Buscar histórico
|
||||||
|
├── POST /api/chat/enviar # Enviar mensagem
|
||||||
|
├── POST /api/webhook/evolution # Receber mensagens
|
||||||
|
|
||||||
|
/sql/
|
||||||
|
├── create-mensagens-whatsapp.sql # Estrutura do banco
|
||||||
|
```
|
||||||
|
|
||||||
|
### **🎯 APIs Disponíveis**
|
||||||
|
|
||||||
|
#### **1. Buscar Histórico**
|
||||||
|
```bash
|
||||||
|
GET /api/chat/5543999762754
|
||||||
|
```
|
||||||
|
**Resposta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"telefone_cliente": "5543999762754",
|
||||||
|
"cliente_nome": "Cliente",
|
||||||
|
"mensagem": "Olá!",
|
||||||
|
"tipo": "enviada",
|
||||||
|
"status": "enviada",
|
||||||
|
"created_at": "2025-01-08T20:30:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **2. Enviar Mensagem**
|
||||||
|
```bash
|
||||||
|
POST /api/chat/enviar
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"telefone": "5543999762754",
|
||||||
|
"mensagem": "Sua mensagem aqui",
|
||||||
|
"clienteNome": "Nome do Cliente"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **3. Webhook (Futuro)**
|
||||||
|
```bash
|
||||||
|
POST /api/webhook/evolution
|
||||||
|
```
|
||||||
|
- Recebe mensagens enviadas pelos clientes
|
||||||
|
- Salva automaticamente no histórico
|
||||||
|
- Atualiza interface em tempo real
|
||||||
|
|
||||||
|
## 🎉 **Benefícios do Sistema**
|
||||||
|
|
||||||
|
### **✅ Para o Negócio**
|
||||||
|
- **Comunicação Centralizada:** Tudo em um lugar
|
||||||
|
- **Histórico Completo:** Nunca perca uma conversa
|
||||||
|
- **Profissionalismo:** Interface moderna e organizada
|
||||||
|
- **Agilidade:** Acesso direto das vendas e clientes
|
||||||
|
- **Automação:** Integração com Evolution API
|
||||||
|
|
||||||
|
### **✅ Para o Usuário**
|
||||||
|
- **Facilidade:** Interface intuitiva como WhatsApp
|
||||||
|
- **Rapidez:** Sem precisar trocar de aplicativo
|
||||||
|
- **Organização:** Conversas organizadas por cliente
|
||||||
|
- **Confiabilidade:** Mensagens sempre entregues
|
||||||
|
- **Flexibilidade:** Funciona mesmo sem Evolution API
|
||||||
|
|
||||||
|
## 🔧 **Configuração Inicial**
|
||||||
|
|
||||||
|
### **1. Evolution API (Recomendado)**
|
||||||
|
1. Acesse: **Configurações → Evolution API**
|
||||||
|
2. Preencha:
|
||||||
|
- **URL Base:** https://sua-evolution-api.com
|
||||||
|
- **Nome da Instância:** SuaInstancia
|
||||||
|
- **API Key:** Sua-Chave-API
|
||||||
|
3. Clique: **Testar Conexão**
|
||||||
|
4. Salve as configurações
|
||||||
|
|
||||||
|
### **2. Teste o Sistema**
|
||||||
|
1. Cadastre um cliente com WhatsApp
|
||||||
|
2. Acesse a lista de clientes
|
||||||
|
3. Clique no botão WhatsApp
|
||||||
|
4. Envie uma mensagem de teste
|
||||||
|
5. Verifique se chegou no WhatsApp
|
||||||
|
|
||||||
|
## 🆘 **Solução de Problemas**
|
||||||
|
|
||||||
|
### **❌ "Evolution API não configurada"**
|
||||||
|
- Acesse Configurações → Evolution API
|
||||||
|
- Verifique se todos os campos estão preenchidos
|
||||||
|
- Teste a conexão com o botão "Testar"
|
||||||
|
|
||||||
|
### **❌ "Cliente não possui WhatsApp"**
|
||||||
|
- Edite o cliente e adicione o número do WhatsApp
|
||||||
|
- Formato: (43) 99976-2754 ou 43999762754
|
||||||
|
|
||||||
|
### **❌ "Erro ao enviar mensagem"**
|
||||||
|
- Verifique sua conexão com a internet
|
||||||
|
- Confirme se a Evolution API está funcionando
|
||||||
|
- O sistema tentará WhatsApp Web como alternativa
|
||||||
|
|
||||||
|
### **❌ "Histórico não carrega"**
|
||||||
|
- Verifique se a tabela mensagens_whatsapp existe
|
||||||
|
- Execute o SQL em sql/create-mensagens-whatsapp.sql
|
||||||
|
- Reinicie o servidor
|
||||||
|
|
||||||
|
## 🎯 **Próximas Funcionalidades**
|
||||||
|
|
||||||
|
### **🔮 Em Desenvolvimento**
|
||||||
|
- **Recebimento Automático:** Mensagens dos clientes aparecem automaticamente
|
||||||
|
- **Notificações:** Alertas de novas mensagens
|
||||||
|
- **Busca:** Pesquisar no histórico de conversas
|
||||||
|
- **Anexos:** Envio de imagens e documentos
|
||||||
|
- **Templates:** Mensagens pré-definidas
|
||||||
|
- **Grupos:** Conversas em grupo
|
||||||
|
|
||||||
|
### **📈 Melhorias Futuras**
|
||||||
|
- **Dashboard de Conversas:** Visão geral de todas as conversas
|
||||||
|
- **Métricas:** Estatísticas de mensagens enviadas/recebidas
|
||||||
|
- **Automação:** Respostas automáticas
|
||||||
|
- **CRM:** Integração completa com vendas
|
||||||
|
- **Mobile:** App mobile nativo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 **Sistema Pronto para Uso!**
|
||||||
|
|
||||||
|
O sistema de chat WhatsApp está **100% funcional** e integrado ao Liberi Kids.
|
||||||
|
|
||||||
|
**Comece a usar agora:**
|
||||||
|
1. Configure a Evolution API
|
||||||
|
2. Acesse Vendas ou Clientes
|
||||||
|
3. Clique no botão WhatsApp
|
||||||
|
4. Comece a conversar!
|
||||||
|
|
||||||
|
**Suporte:** Em caso de dúvidas, consulte este guia ou entre em contato.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Sistema desenvolvido para Liberi Kids - Controle de Estoque Inteligente* 🎯✨
|
||||||
309
DEPLOY-GUIDE.md
Normal file
309
DEPLOY-GUIDE.md
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# 🚀 Guia de Deploy - Liberi Kids Sistema de Estoque
|
||||||
|
|
||||||
|
## 📋 Pré-requisitos
|
||||||
|
|
||||||
|
- Node.js 18+ instalado
|
||||||
|
- Conta no Supabase (já configurada)
|
||||||
|
- Dados de acesso ao Supabase
|
||||||
|
|
||||||
|
## 🌐 Opções de Deploy
|
||||||
|
|
||||||
|
### 1. 🖥️ **SERVIDOR LOCAL (Produção)**
|
||||||
|
|
||||||
|
#### Preparação do Build
|
||||||
|
```bash
|
||||||
|
# 1. Entre na pasta do projeto
|
||||||
|
cd /home/tiago/Downloads/app_estoque
|
||||||
|
|
||||||
|
# 2. Instale dependências (se não instaladas)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 3. Entre na pasta do cliente
|
||||||
|
cd client
|
||||||
|
|
||||||
|
# 4. Instale dependências do frontend
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 5. Faça o build de produção
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 6. Volte para a pasta raiz
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configuração do Servidor
|
||||||
|
```bash
|
||||||
|
# 1. Crie arquivo de configuração de produção
|
||||||
|
cp .env.example .env.production
|
||||||
|
|
||||||
|
# 2. Edite as variáveis de ambiente
|
||||||
|
nano .env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conteúdo do .env.production:**
|
||||||
|
```env
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=5000
|
||||||
|
SUPABASE_URL=sua_url_do_supabase
|
||||||
|
SUPABASE_ANON_KEY=sua_chave_anonima_do_supabase
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Executar em Produção
|
||||||
|
```bash
|
||||||
|
# Opção 1: Execução simples
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Opção 2: Com PM2 (recomendado)
|
||||||
|
npm install -g pm2
|
||||||
|
pm2 start server-supabase.js --name "liberi-kids"
|
||||||
|
pm2 startup
|
||||||
|
pm2 save
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configurar Nginx (Opcional)
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name seu-dominio.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:5000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. ☁️ **DEPLOY EM NUVEM**
|
||||||
|
|
||||||
|
#### A. **Vercel (Recomendado - Gratuito)**
|
||||||
|
|
||||||
|
1. **Preparação:**
|
||||||
|
```bash
|
||||||
|
# 1. Instale Vercel CLI
|
||||||
|
npm install -g vercel
|
||||||
|
|
||||||
|
# 2. Faça login
|
||||||
|
vercel login
|
||||||
|
|
||||||
|
# 3. Configure o projeto
|
||||||
|
vercel
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configuração no Vercel:**
|
||||||
|
- Build Command: `cd client && npm run build`
|
||||||
|
- Output Directory: `client/build`
|
||||||
|
- Install Command: `npm install && cd client && npm install`
|
||||||
|
|
||||||
|
3. **Variáveis de Ambiente no Vercel:**
|
||||||
|
```
|
||||||
|
SUPABASE_URL=sua_url_do_supabase
|
||||||
|
SUPABASE_ANON_KEY=sua_chave_anonima_do_supabase
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B. **Netlify (Alternativa Gratuita)**
|
||||||
|
|
||||||
|
1. **Via Git:**
|
||||||
|
- Conecte seu repositório GitHub
|
||||||
|
- Build Command: `cd client && npm run build`
|
||||||
|
- Publish Directory: `client/build`
|
||||||
|
|
||||||
|
2. **Via CLI:**
|
||||||
|
```bash
|
||||||
|
npm install -g netlify-cli
|
||||||
|
netlify login
|
||||||
|
netlify deploy --prod --dir=client/build
|
||||||
|
```
|
||||||
|
|
||||||
|
#### C. **Railway (Backend + Frontend)**
|
||||||
|
|
||||||
|
1. **Preparação:**
|
||||||
|
```bash
|
||||||
|
# 1. Instale Railway CLI
|
||||||
|
npm install -g @railway/cli
|
||||||
|
|
||||||
|
# 2. Login
|
||||||
|
railway login
|
||||||
|
|
||||||
|
# 3. Deploy
|
||||||
|
railway deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
#### D. **Heroku (Pago)**
|
||||||
|
|
||||||
|
1. **Preparação:**
|
||||||
|
```bash
|
||||||
|
# 1. Instale Heroku CLI
|
||||||
|
# 2. Login
|
||||||
|
heroku login
|
||||||
|
|
||||||
|
# 3. Crie app
|
||||||
|
heroku create liberi-kids-estoque
|
||||||
|
|
||||||
|
# 4. Configure variáveis
|
||||||
|
heroku config:set SUPABASE_URL=sua_url
|
||||||
|
heroku config:set SUPABASE_ANON_KEY=sua_chave
|
||||||
|
|
||||||
|
# 5. Deploy
|
||||||
|
git push heroku main
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 🐳 **DOCKER (Containerização)**
|
||||||
|
|
||||||
|
#### Dockerfile
|
||||||
|
```dockerfile
|
||||||
|
# Dockerfile
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copiar package.json
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copiar código do servidor
|
||||||
|
COPY server-supabase.js ./
|
||||||
|
COPY sql/ ./sql/
|
||||||
|
|
||||||
|
# Copiar e buildar frontend
|
||||||
|
COPY client/ ./client/
|
||||||
|
WORKDIR /app/client
|
||||||
|
RUN npm install && npm run build
|
||||||
|
|
||||||
|
# Voltar para raiz
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["node", "server-supabase.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker Compose
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
liberi-kids:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- SUPABASE_URL=${SUPABASE_URL}
|
||||||
|
- SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Comandos Docker
|
||||||
|
```bash
|
||||||
|
# Build e run
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Ou build manual
|
||||||
|
docker build -t liberi-kids .
|
||||||
|
docker run -p 5000:5000 -e SUPABASE_URL=sua_url liberi-kids
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 **Configurações Importantes**
|
||||||
|
|
||||||
|
### 1. **Variáveis de Ambiente**
|
||||||
|
```env
|
||||||
|
# Obrigatórias
|
||||||
|
SUPABASE_URL=https://seu-projeto.supabase.co
|
||||||
|
SUPABASE_ANON_KEY=sua_chave_anonima
|
||||||
|
|
||||||
|
# Opcionais
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Configuração do Supabase**
|
||||||
|
- Certifique-se que as políticas RLS estão configuradas
|
||||||
|
- Verifique se todas as tabelas foram criadas
|
||||||
|
- Configure CORS se necessário
|
||||||
|
|
||||||
|
### 3. **Segurança**
|
||||||
|
- Use HTTPS em produção
|
||||||
|
- Configure firewall adequadamente
|
||||||
|
- Mantenha as chaves do Supabase seguras
|
||||||
|
- Use variáveis de ambiente para dados sensíveis
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 **Acesso Móvel**
|
||||||
|
|
||||||
|
### PWA (Progressive Web App)
|
||||||
|
A aplicação já está configurada como PWA:
|
||||||
|
- Funciona offline (parcialmente)
|
||||||
|
- Pode ser instalada no celular
|
||||||
|
- Interface responsiva
|
||||||
|
|
||||||
|
### Configuração de Domínio
|
||||||
|
```bash
|
||||||
|
# Para usar domínio próprio
|
||||||
|
# 1. Configure DNS apontando para seu servidor
|
||||||
|
# 2. Configure SSL/TLS (Let's Encrypt)
|
||||||
|
# 3. Atualize configurações do Supabase se necessário
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 **Troubleshooting**
|
||||||
|
|
||||||
|
### Problemas Comuns:
|
||||||
|
1. **Erro de CORS:** Configure headers no servidor
|
||||||
|
2. **Build falha:** Verifique dependências do Node.js
|
||||||
|
3. **Supabase não conecta:** Verifique URLs e chaves
|
||||||
|
4. **Arquivos não encontrados:** Verifique paths do build
|
||||||
|
|
||||||
|
### Logs e Monitoramento:
|
||||||
|
```bash
|
||||||
|
# PM2 logs
|
||||||
|
pm2 logs liberi-kids
|
||||||
|
|
||||||
|
# Docker logs
|
||||||
|
docker logs container-name
|
||||||
|
|
||||||
|
# Vercel logs
|
||||||
|
vercel logs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 **Recomendações**
|
||||||
|
|
||||||
|
### Para Pequenas Empresas:
|
||||||
|
1. **Vercel** (frontend) + **Supabase** (backend/DB) - **GRATUITO**
|
||||||
|
2. **Railway** (fullstack) - **Barato e simples**
|
||||||
|
|
||||||
|
### Para Empresas Médias:
|
||||||
|
1. **VPS** (DigitalOcean, Linode) + **Docker**
|
||||||
|
2. **AWS/Google Cloud** com auto-scaling
|
||||||
|
|
||||||
|
### Para Desenvolvimento:
|
||||||
|
1. **Servidor local** com PM2
|
||||||
|
2. **Docker** para ambiente consistente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 **Suporte**
|
||||||
|
|
||||||
|
Se precisar de ajuda com o deploy:
|
||||||
|
1. Verifique os logs de erro
|
||||||
|
2. Confirme configurações do Supabase
|
||||||
|
3. Teste APIs individualmente
|
||||||
|
4. Verifique conectividade de rede
|
||||||
|
|
||||||
|
**A aplicação está pronta para produção! 🎉**
|
||||||
178
DEPLOY-PIX-WORKFLOW.md
Normal file
178
DEPLOY-PIX-WORKFLOW.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# 🚀 Workflow Deploy PIX - Local para Servidor
|
||||||
|
|
||||||
|
## 📋 **Seu Fluxo de Trabalho**
|
||||||
|
|
||||||
|
Você desenvolve na **máquina local** e depois faz **deploy para o servidor**. Este guia é específico para essa situação.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ **Deploy Automático (Recomendado)**
|
||||||
|
|
||||||
|
### **1. Execute o Script de Deploy:**
|
||||||
|
```bash
|
||||||
|
# Na sua máquina local:
|
||||||
|
cd /home/tiago/Downloads/app_estoque
|
||||||
|
./deploy-pix-completo.sh tiago@192.168.195.145
|
||||||
|
```
|
||||||
|
|
||||||
|
**O script fará automaticamente:**
|
||||||
|
- ✅ Build do frontend React
|
||||||
|
- ✅ Envio de todos os arquivos PIX
|
||||||
|
- ✅ Instalação das dependências no servidor
|
||||||
|
- ✅ Configuração inicial do .env
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 **Configuração Final no Servidor**
|
||||||
|
|
||||||
|
### **2. Configurar Credenciais Mercado Pago:**
|
||||||
|
```bash
|
||||||
|
# Conecte no servidor:
|
||||||
|
ssh tiago@192.168.195.145
|
||||||
|
cd ~/app_estoque
|
||||||
|
|
||||||
|
# Edite o .env:
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# Configure estas linhas:
|
||||||
|
MERCADOPAGO_ACCESS_TOKEN=TEST-sua_access_token_aqui
|
||||||
|
MERCADOPAGO_PUBLIC_KEY=pk_test_sua_public_key_aqui
|
||||||
|
BASE_URL=http://192.168.195.145:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. Aplicar SQL no Supabase:**
|
||||||
|
- Acesse: https://supabase.com/dashboard
|
||||||
|
- Vá em **SQL Editor**
|
||||||
|
- Execute o conteúdo do arquivo: `aplicar-pix-supabase.sql`
|
||||||
|
|
||||||
|
### **4. Reiniciar Servidor:**
|
||||||
|
```bash
|
||||||
|
# No servidor:
|
||||||
|
pm2 restart liberi-kids
|
||||||
|
# ou se não estiver rodando:
|
||||||
|
pm2 start server-supabase.js --name liberi-kids
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏦 **Obter Credenciais Mercado Pago**
|
||||||
|
|
||||||
|
1. **Acesse:** https://www.mercadopago.com.br/developers
|
||||||
|
2. **Faça login** ou crie conta
|
||||||
|
3. **Vá em:** "Suas integrações" → "Criar aplicação"
|
||||||
|
4. **Escolha:** "Pagamentos online"
|
||||||
|
5. **Copie:**
|
||||||
|
- **Access Token:** `TEST-1234567890-abcdef...`
|
||||||
|
- **Public Key:** `pk_test_1234567890abcdef...`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 **Estrutura de Arquivos Enviados**
|
||||||
|
|
||||||
|
```
|
||||||
|
servidor:~/app_estoque/
|
||||||
|
├── server-supabase.js # Backend atualizado com PIX
|
||||||
|
├── config/
|
||||||
|
│ └── mercadopago.js # Serviço PIX
|
||||||
|
├── client/build/ # Frontend compilado
|
||||||
|
├── sql/
|
||||||
|
│ └── add-pix-fields.sql # Migração banco
|
||||||
|
├── scripts/
|
||||||
|
│ └── migrate-pix-fields.js # Script migração
|
||||||
|
├── aplicar-pix-supabase.sql # SQL direto Supabase
|
||||||
|
├── configurar-pix-servidor.sh # Script configuração
|
||||||
|
└── .env # Configurações (você edita)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 **Como Testar**
|
||||||
|
|
||||||
|
1. **Acesse o sistema** no servidor
|
||||||
|
2. **Vá em Vendas**
|
||||||
|
3. **Clique no botão PIX** (ícone 💳)
|
||||||
|
4. **Verifique se:**
|
||||||
|
- Modal PIX abre
|
||||||
|
- QR Code é gerado
|
||||||
|
- Código PIX aparece para copiar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 **Próximos Deploys**
|
||||||
|
|
||||||
|
Para **atualizações futuras**, você pode:
|
||||||
|
|
||||||
|
### **Opção 1: Script Completo (Recomendado)**
|
||||||
|
```bash
|
||||||
|
./deploy-pix-completo.sh tiago@192.168.195.145
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Opção 2: Envio Manual**
|
||||||
|
```bash
|
||||||
|
# Build local:
|
||||||
|
cd client && npm run build && cd ..
|
||||||
|
|
||||||
|
# Envio específico:
|
||||||
|
rsync -avz --progress server-supabase.js tiago@192.168.195.145:~/app_estoque/
|
||||||
|
rsync -avz --progress client/build/ tiago@192.168.195.145:~/app_estoque/client/build/
|
||||||
|
rsync -avz --progress config/ tiago@192.168.195.145:~/app_estoque/config/
|
||||||
|
|
||||||
|
# Reiniciar no servidor:
|
||||||
|
ssh tiago@192.168.195.145 "cd ~/app_estoque && pm2 restart liberi-kids"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ **Troubleshooting**
|
||||||
|
|
||||||
|
### **Erro de Conexão SSH:**
|
||||||
|
```bash
|
||||||
|
# Teste a conexão:
|
||||||
|
ssh tiago@192.168.195.145
|
||||||
|
|
||||||
|
# Se não funcionar, verifique:
|
||||||
|
# - Servidor ligado?
|
||||||
|
# - SSH funcionando?
|
||||||
|
# - Credenciais corretas?
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Erro no Build:**
|
||||||
|
```bash
|
||||||
|
# Na máquina local:
|
||||||
|
cd client
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Erro no Servidor:**
|
||||||
|
```bash
|
||||||
|
# No servidor:
|
||||||
|
ssh tiago@192.168.195.145
|
||||||
|
cd ~/app_estoque
|
||||||
|
npm install
|
||||||
|
pm2 logs liberi-kids
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 **Dicas Importantes**
|
||||||
|
|
||||||
|
1. **Sempre faça build local** antes do deploy
|
||||||
|
2. **Mantenha credenciais seguras** no .env do servidor
|
||||||
|
3. **Use credenciais TEST** para desenvolvimento
|
||||||
|
4. **Configure webhook URL** no Mercado Pago para produção
|
||||||
|
5. **Monitore logs** com `pm2 logs liberi-kids`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 **Resultado Final**
|
||||||
|
|
||||||
|
Após seguir este workflow, você terá:
|
||||||
|
|
||||||
|
- ✅ **PIX funcionando** no servidor
|
||||||
|
- ✅ **QR Code automático** nas vendas
|
||||||
|
- ✅ **Confirmação em tempo real**
|
||||||
|
- ✅ **Interface moderna** e responsiva
|
||||||
|
- ✅ **Deploy automatizado** para futuras atualizações
|
||||||
|
|
||||||
|
**Seu sistema estará pronto para aceitar pagamentos PIX!** 🏦💳
|
||||||
216
DEPLOY-SSH-GUIDE.md
Normal file
216
DEPLOY-SSH-GUIDE.md
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
# 🚀 Guia de Deploy via SSH - Liberi Kids
|
||||||
|
|
||||||
|
## ⚡ Deploy Automático em 2 Comandos
|
||||||
|
|
||||||
|
### **Método 1: Script Automatizado (Recomendado)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Execute o script automático
|
||||||
|
./deploy-to-server.sh usuario@ip-do-servidor
|
||||||
|
|
||||||
|
# Exemplo:
|
||||||
|
./deploy-to-server.sh root@192.168.1.100
|
||||||
|
./deploy-to-server.sh ubuntu@meuservidor.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Método 2: Manual Passo a Passo**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Copiar arquivos para o servidor
|
||||||
|
rsync -avz --progress --exclude 'node_modules' ./ usuario@servidor:~/app_estoque/
|
||||||
|
|
||||||
|
# 2. Conectar no servidor
|
||||||
|
ssh usuario@servidor
|
||||||
|
|
||||||
|
# 3. Navegar para o projeto
|
||||||
|
cd app_estoque
|
||||||
|
|
||||||
|
# 4. Configurar .env
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env # Configure suas credenciais
|
||||||
|
|
||||||
|
# 5. Executar deploy
|
||||||
|
chmod +x scripts/deploy-local.sh
|
||||||
|
./scripts/deploy-local.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 **Configuração do Arquivo .env**
|
||||||
|
|
||||||
|
Após o deploy, você DEVE configurar o arquivo `.env` no servidor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Conecte no servidor e edite
|
||||||
|
ssh usuario@servidor
|
||||||
|
cd app_estoque
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configure estas variáveis:**
|
||||||
|
```env
|
||||||
|
# Suas credenciais do Supabase
|
||||||
|
SUPABASE_URL=https://seu-projeto.supabase.co
|
||||||
|
SUPABASE_ANON_KEY=sua_chave_anonima_aqui
|
||||||
|
|
||||||
|
# Configurações do servidor
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=5000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Após configurar, reinicie:**
|
||||||
|
```bash
|
||||||
|
pm2 restart liberi-kids
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 **Acessar a Aplicação**
|
||||||
|
|
||||||
|
Após o deploy bem-sucedido:
|
||||||
|
|
||||||
|
- **Local:** `http://localhost:5000` (no servidor)
|
||||||
|
- **Rede:** `http://IP-DO-SERVIDOR:5000`
|
||||||
|
- **Domínio:** `http://seu-dominio.com:5000`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 **Comandos Úteis**
|
||||||
|
|
||||||
|
### **Gerenciar Aplicação:**
|
||||||
|
```bash
|
||||||
|
# Ver status
|
||||||
|
ssh usuario@servidor 'pm2 status'
|
||||||
|
|
||||||
|
# Ver logs
|
||||||
|
ssh usuario@servidor 'pm2 logs liberi-kids'
|
||||||
|
|
||||||
|
# Reiniciar
|
||||||
|
ssh usuario@servidor 'pm2 restart liberi-kids'
|
||||||
|
|
||||||
|
# Parar
|
||||||
|
ssh usuario@servidor 'pm2 stop liberi-kids'
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Atualizar Aplicação:**
|
||||||
|
```bash
|
||||||
|
# Re-executar o script de deploy
|
||||||
|
./deploy-to-server.sh usuario@servidor
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 **Configurações de Segurança**
|
||||||
|
|
||||||
|
### **Firewall (Ubuntu/Debian):**
|
||||||
|
```bash
|
||||||
|
ssh usuario@servidor '
|
||||||
|
sudo ufw allow 5000
|
||||||
|
sudo ufw enable
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Nginx (Opcional - Para usar porta 80):**
|
||||||
|
```bash
|
||||||
|
ssh usuario@servidor '
|
||||||
|
sudo apt install nginx
|
||||||
|
sudo nano /etc/nginx/sites-available/liberi-kids
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuração Nginx:**
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name seu-dominio.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:5000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 **Resolução de Problemas**
|
||||||
|
|
||||||
|
### **Erro: "Permission denied"**
|
||||||
|
```bash
|
||||||
|
# Solução: Configurar chaves SSH
|
||||||
|
ssh-copy-id usuario@servidor
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Erro: "Port 5000 already in use"**
|
||||||
|
```bash
|
||||||
|
ssh usuario@servidor 'sudo lsof -ti:5000 | xargs kill -9'
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Erro: "Node.js not found"**
|
||||||
|
```bash
|
||||||
|
ssh usuario@servidor '
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||||
|
sudo apt-get install -y nodejs
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Erro: "PM2 not found"**
|
||||||
|
```bash
|
||||||
|
ssh usuario@servidor 'npm install -g pm2'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 **Recursos da Aplicação**
|
||||||
|
|
||||||
|
Após o deploy, você terá acesso a:
|
||||||
|
|
||||||
|
- ✅ **Dashboard Completo** com contabilidade real
|
||||||
|
- ✅ **Controle de Produtos** com fotos e variações
|
||||||
|
- ✅ **Gestão de Vendas** com WhatsApp integrado
|
||||||
|
- ✅ **Sistema de Empréstimos** para Maiara
|
||||||
|
- ✅ **Exportação Google Sheets** automática
|
||||||
|
- ✅ **Alertas WhatsApp** para cobranças
|
||||||
|
- ✅ **Interface Responsiva** (funciona no celular)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **Checklist Pós-Deploy**
|
||||||
|
|
||||||
|
- [ ] ✅ Aplicação rodando (pm2 status)
|
||||||
|
- [ ] ✅ Arquivo .env configurado
|
||||||
|
- [ ] ✅ Supabase conectado
|
||||||
|
- [ ] ✅ Firewall configurado
|
||||||
|
- [ ] ✅ Backup configurado
|
||||||
|
- [ ] ✅ Domínio configurado (opcional)
|
||||||
|
- [ ] ✅ SSL configurado (opcional)
|
||||||
|
- [ ] ✅ Usuários treinados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 **Suporte**
|
||||||
|
|
||||||
|
### **Logs de Erro:**
|
||||||
|
```bash
|
||||||
|
ssh usuario@servidor 'pm2 logs liberi-kids --lines 50'
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Monitoramento:**
|
||||||
|
```bash
|
||||||
|
ssh usuario@servidor 'pm2 monit'
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Backup Manual:**
|
||||||
|
```bash
|
||||||
|
ssh usuario@servidor 'cd app_estoque && tar -czf backup-$(date +%Y%m%d).tar.gz .'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🎉 Sua aplicação Liberi Kids estará rodando em produção!**
|
||||||
|
|
||||||
|
Para suporte detalhado, consulte: `DEPLOY-GUIDE.md`
|
||||||
81
DESPESAS-CAMPOS-LIVRES.md
Normal file
81
DESPESAS-CAMPOS-LIVRES.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# 📝 Configuração de Campos Livres para Despesas
|
||||||
|
|
||||||
|
## 🎯 Objetivo
|
||||||
|
Permitir que os campos "Tipo de Despesa" e "Fornecedor" sejam campos de texto livre, onde você pode digitar qualquer nome sem precisar cadastrar previamente.
|
||||||
|
|
||||||
|
## 🔧 Passo a Passo
|
||||||
|
|
||||||
|
### 1. Adicionar Colunas no Supabase Dashboard
|
||||||
|
|
||||||
|
Acesse o **Supabase Dashboard** → **Table Editor** → **despesas** e adicione as seguintes colunas:
|
||||||
|
|
||||||
|
#### Coluna: `tipo_nome`
|
||||||
|
- **Nome:** `tipo_nome`
|
||||||
|
- **Tipo:** `text`
|
||||||
|
- **Nullable:** ✅ Sim
|
||||||
|
- **Default:** (vazio)
|
||||||
|
|
||||||
|
#### Coluna: `fornecedor_nome`
|
||||||
|
- **Nome:** `fornecedor_nome`
|
||||||
|
- **Tipo:** `text`
|
||||||
|
- **Nullable:** ✅ Sim
|
||||||
|
- **Default:** (vazio)
|
||||||
|
|
||||||
|
### 2. SQL Alternativo (se preferir)
|
||||||
|
|
||||||
|
Se preferir executar via SQL Editor no Supabase:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Adicionar colunas de texto livre
|
||||||
|
ALTER TABLE despesas ADD COLUMN IF NOT EXISTS tipo_nome TEXT;
|
||||||
|
ALTER TABLE despesas ADD COLUMN IF NOT EXISTS fornecedor_nome TEXT;
|
||||||
|
|
||||||
|
-- Migrar dados existentes (opcional)
|
||||||
|
UPDATE despesas
|
||||||
|
SET tipo_nome = (
|
||||||
|
SELECT nome FROM tipos_despesas
|
||||||
|
WHERE tipos_despesas.id = despesas.tipo_despesa_id
|
||||||
|
)
|
||||||
|
WHERE tipo_despesa_id IS NOT NULL AND tipo_nome IS NULL;
|
||||||
|
|
||||||
|
UPDATE despesas
|
||||||
|
SET fornecedor_nome = (
|
||||||
|
SELECT razao_social FROM fornecedores
|
||||||
|
WHERE fornecedores.id = despesas.fornecedor_id
|
||||||
|
)
|
||||||
|
WHERE fornecedor_id IS NOT NULL AND fornecedor_nome IS NULL;
|
||||||
|
|
||||||
|
-- Criar índices para performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_despesas_tipo_nome ON despesas(tipo_nome);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_despesas_fornecedor_nome ON despesas(fornecedor_nome);
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Verificação
|
||||||
|
|
||||||
|
Após adicionar as colunas, teste criando uma nova despesa:
|
||||||
|
|
||||||
|
1. Vá para **Despesas** → **Nova Despesa**
|
||||||
|
2. Digite qualquer nome no campo **Tipo de Despesa** (ex: "Manutenção do Ar")
|
||||||
|
3. Digite qualquer nome no campo **Fornecedor** (ex: "João Técnico")
|
||||||
|
4. Preencha os outros campos e salve
|
||||||
|
|
||||||
|
## 🎉 Resultado
|
||||||
|
|
||||||
|
Agora você pode:
|
||||||
|
- ✅ Digitar qualquer tipo de despesa sem cadastrar previamente
|
||||||
|
- ✅ Digitar qualquer fornecedor sem cadastrar previamente
|
||||||
|
- ✅ Ter total liberdade nos nomes
|
||||||
|
- ✅ Continuar usando a interface normalmente
|
||||||
|
|
||||||
|
## 🔄 Status da Implementação
|
||||||
|
|
||||||
|
- ✅ **Frontend:** Campos alterados para input de texto livre
|
||||||
|
- ✅ **Backend:** APIs atualizadas para aceitar texto livre
|
||||||
|
- ⏳ **Banco:** Aguardando adição das colunas (manual)
|
||||||
|
|
||||||
|
## 📞 Suporte
|
||||||
|
|
||||||
|
Após adicionar as colunas, o sistema funcionará automaticamente. Se houver algum problema, verifique se:
|
||||||
|
1. As colunas foram criadas corretamente
|
||||||
|
2. O servidor foi reiniciado
|
||||||
|
3. Não há erros no console do navegador
|
||||||
171
DEVOLUCOES-CORRIGIDAS.md
Normal file
171
DEVOLUCOES-CORRIGIDAS.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# 🔧 Correções no Sistema de Devoluções/Trocas
|
||||||
|
|
||||||
|
## Problemas Identificados e Corrigidos
|
||||||
|
|
||||||
|
### ❌ **Problema Original:**
|
||||||
|
- Devoluções acumulavam registros em vez de devolver produtos ao estoque
|
||||||
|
- Trocas não controlavam corretamente o estoque (entrada/saída)
|
||||||
|
- Possibilidade de devolver mais itens do que foi vendido
|
||||||
|
- Registros duplicados de devoluções
|
||||||
|
|
||||||
|
### ✅ **Soluções Implementadas:**
|
||||||
|
|
||||||
|
## 1. **Controle de Estoque em Devoluções**
|
||||||
|
|
||||||
|
### **Antes:**
|
||||||
|
```javascript
|
||||||
|
// Sempre retornava ao estoque, mesmo em trocas
|
||||||
|
const novoEstoque = estoqueAtual + quantidadeDevolvida;
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Depois:**
|
||||||
|
```javascript
|
||||||
|
// Só retorna ao estoque se for devolução pura (não troca)
|
||||||
|
if (itemOriginal.variacao_id && tipo_operacao !== 'troca') {
|
||||||
|
const novoEstoque = (variacaoAtual.quantidade || 0) + parseInt(quantidade_devolvida);
|
||||||
|
// Atualizar estoque...
|
||||||
|
console.log(`✅ Estoque atualizado: +${quantidade_devolvida} unidades`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. **Controle de Estoque em Trocas**
|
||||||
|
|
||||||
|
### **Lógica Corrigida:**
|
||||||
|
1. **Produto Original (devolvido):** Volta ao estoque
|
||||||
|
2. **Produto Novo (trocado):** Sai do estoque
|
||||||
|
3. **Registro:** Apenas um registro de devolução + item novo na venda
|
||||||
|
|
||||||
|
### **Implementação:**
|
||||||
|
```javascript
|
||||||
|
// Para trocas, devolver produto original ao estoque
|
||||||
|
if (itemOriginal && itemOriginal.variacao_id) {
|
||||||
|
const novoEstoque = (variacaoAtual.quantidade || 0) + parseInt(quantidade_devolvida);
|
||||||
|
console.log(`✅ Produto original retornado ao estoque na troca: +${quantidade_devolvida} unidades`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduzir estoque do produto novo
|
||||||
|
const novoEstoque = (variacaoAtual.quantidade || 0) - parseInt(quantidade);
|
||||||
|
console.log(`✅ Estoque reduzido na troca: -${quantidade} unidades`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. **Validação Anti-Duplicação**
|
||||||
|
|
||||||
|
### **Verificação de Devoluções Anteriores:**
|
||||||
|
```javascript
|
||||||
|
// Verificar se já foi devolvido anteriormente
|
||||||
|
const { data: devolucaoExistente } = await supabase
|
||||||
|
.from('devolucoes')
|
||||||
|
.select('quantidade_devolvida')
|
||||||
|
.eq('venda_id', venda_id)
|
||||||
|
.eq('item_id', item_id);
|
||||||
|
|
||||||
|
const quantidadeJaDevolvida = devolucaoExistente?.reduce((total, dev) =>
|
||||||
|
total + parseInt(dev.quantidade_devolvida), 0) || 0;
|
||||||
|
|
||||||
|
const quantidadeDisponivel = parseInt(itemOriginal.quantidade) - quantidadeJaDevolvida;
|
||||||
|
|
||||||
|
if (parseInt(quantidade_devolvida) > quantidadeDisponivel) {
|
||||||
|
throw new Error(`Quantidade de devolução (${quantidade_devolvida}) maior que disponível (${quantidadeDisponivel})`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. **API de Limpeza de Duplicatas**
|
||||||
|
|
||||||
|
### **Nova Rota:**
|
||||||
|
```
|
||||||
|
POST /api/devolucoes/limpar-duplicadas
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Funcionalidade:**
|
||||||
|
- Identifica devoluções duplicadas (mesmo venda_id, item_id, data)
|
||||||
|
- Remove registros duplicados automaticamente
|
||||||
|
- Retorna quantidade de registros removidos
|
||||||
|
|
||||||
|
## 5. **Logs Detalhados**
|
||||||
|
|
||||||
|
### **Monitoramento em Tempo Real:**
|
||||||
|
```javascript
|
||||||
|
console.log(`✅ Estoque atualizado: +${quantidade_devolvida} unidades (novo total: ${novoEstoque})`);
|
||||||
|
console.log(`✅ Estoque reduzido na troca: -${quantidade} unidades (novo total: ${novoEstoque})`);
|
||||||
|
console.log(`✅ Produto original retornado ao estoque na troca: +${quantidade_devolvida} unidades`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 **Fluxos Corrigidos**
|
||||||
|
|
||||||
|
### **Devolução Simples:**
|
||||||
|
1. ✅ Verifica se quantidade não excede o vendido
|
||||||
|
2. ✅ Retorna produto ao estoque
|
||||||
|
3. ✅ Registra devolução única
|
||||||
|
4. ✅ Atualiza valor da venda
|
||||||
|
|
||||||
|
### **Troca de Produto:**
|
||||||
|
1. ✅ Verifica estoque do produto novo
|
||||||
|
2. ✅ Retorna produto original ao estoque
|
||||||
|
3. ✅ Reduz estoque do produto novo
|
||||||
|
4. ✅ Adiciona item novo à venda
|
||||||
|
5. ✅ Registra devolução do item original
|
||||||
|
6. ✅ Calcula diferença de valores
|
||||||
|
|
||||||
|
## 🧪 **Como Testar**
|
||||||
|
|
||||||
|
### **1. Teste de Devolução:**
|
||||||
|
```bash
|
||||||
|
# Executar script de verificação
|
||||||
|
node scripts/fix-devolucoes.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Teste Manual:**
|
||||||
|
1. Acesse **Devoluções/Trocas**
|
||||||
|
2. Selecione uma venda
|
||||||
|
3. Escolha "Devolução"
|
||||||
|
4. Verifique se o estoque aumentou
|
||||||
|
5. Tente devolver o mesmo item novamente (deve dar erro)
|
||||||
|
|
||||||
|
### **3. Teste de Troca:**
|
||||||
|
1. Selecione uma venda
|
||||||
|
2. Escolha "Troca"
|
||||||
|
3. Selecione produto para devolver
|
||||||
|
4. Selecione produto novo
|
||||||
|
5. Verifique:
|
||||||
|
- Produto original voltou ao estoque
|
||||||
|
- Produto novo saiu do estoque
|
||||||
|
- Venda foi atualizada com novo item
|
||||||
|
|
||||||
|
## 🔍 **Verificações de Integridade**
|
||||||
|
|
||||||
|
### **Comandos Úteis:**
|
||||||
|
```sql
|
||||||
|
-- Verificar devoluções por venda
|
||||||
|
SELECT venda_id, COUNT(*) as total_devolucoes
|
||||||
|
FROM devolucoes
|
||||||
|
GROUP BY venda_id
|
||||||
|
HAVING COUNT(*) > 1;
|
||||||
|
|
||||||
|
-- Verificar estoque negativo
|
||||||
|
SELECT * FROM produto_variacoes WHERE quantidade < 0;
|
||||||
|
|
||||||
|
-- Verificar devoluções duplicadas
|
||||||
|
SELECT venda_id, item_id, data_devolucao, COUNT(*)
|
||||||
|
FROM devolucoes
|
||||||
|
GROUP BY venda_id, item_id, data_devolucao
|
||||||
|
HAVING COUNT(*) > 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 **Benefícios das Correções**
|
||||||
|
|
||||||
|
- ✅ **Estoque Preciso:** Controle real de entrada/saída
|
||||||
|
- ✅ **Sem Duplicatas:** Prevenção de registros duplicados
|
||||||
|
- ✅ **Validação Robusta:** Impossível devolver mais que vendido
|
||||||
|
- ✅ **Logs Detalhados:** Rastreamento completo das operações
|
||||||
|
- ✅ **Trocas Corretas:** Fluxo de estoque adequado
|
||||||
|
- ✅ **Limpeza Automática:** Ferramenta para corrigir dados históricos
|
||||||
|
|
||||||
|
## 🚀 **Status**
|
||||||
|
|
||||||
|
**✅ IMPLEMENTADO E FUNCIONANDO**
|
||||||
|
|
||||||
|
O sistema agora controla corretamente:
|
||||||
|
- Devoluções simples com retorno ao estoque
|
||||||
|
- Trocas com controle bidirecional de estoque
|
||||||
|
- Prevenção de duplicatas e validações
|
||||||
|
- Limpeza de dados históricos inconsistentes
|
||||||
58
Dockerfile
Normal file
58
Dockerfile
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# 🐳 Dockerfile - Liberi Kids Sistema de Estoque
|
||||||
|
|
||||||
|
# Usar imagem oficial do Node.js 18 Alpine (menor tamanho)
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
# Definir diretório de trabalho
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Instalar dependências do sistema (se necessário)
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++
|
||||||
|
|
||||||
|
# Copiar arquivos de dependências do servidor
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Instalar dependências do servidor
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copiar código do servidor
|
||||||
|
COPY server-supabase.js ./
|
||||||
|
COPY sql/ ./sql/
|
||||||
|
|
||||||
|
# Copiar arquivos do frontend
|
||||||
|
COPY client/package*.json ./client/
|
||||||
|
WORKDIR /app/client
|
||||||
|
|
||||||
|
# Instalar dependências do frontend
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copiar código do frontend
|
||||||
|
COPY client/src/ ./src/
|
||||||
|
COPY client/public/ ./public/
|
||||||
|
|
||||||
|
# Fazer build do frontend
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Voltar para diretório raiz
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Criar usuário não-root para segurança
|
||||||
|
RUN addgroup -g 1001 -S nodejs
|
||||||
|
RUN adduser -S nextjs -u 1001
|
||||||
|
|
||||||
|
# Mudar ownership dos arquivos
|
||||||
|
RUN chown -R nextjs:nodejs /app
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
# Expor porta
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Definir variáveis de ambiente padrão
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=5000
|
||||||
|
|
||||||
|
# Comando de inicialização
|
||||||
|
CMD ["node", "server-supabase.js"]
|
||||||
145
EMPRESTIMOS-SETUP.md
Normal file
145
EMPRESTIMOS-SETUP.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# 💰 Sistema de Empréstimos - Guia de Configuração
|
||||||
|
|
||||||
|
## 📋 Sobre o Sistema
|
||||||
|
|
||||||
|
O sistema de empréstimos permite controlar o dinheiro que a Maiara pega emprestado da empresa e suas devoluções parciais ou totais.
|
||||||
|
|
||||||
|
### 🎯 Funcionalidades:
|
||||||
|
|
||||||
|
- ✅ **Registro de empréstimos** com valor total e descrição
|
||||||
|
- ✅ **Devoluções parciais** com qualquer valor até quitar
|
||||||
|
- ✅ **Controle automático** do valor restante
|
||||||
|
- ✅ **Histórico completo** de todas as devoluções
|
||||||
|
- ✅ **Status automático** (Ativo/Quitado)
|
||||||
|
- ✅ **Dashboard visual** com resumos financeiros
|
||||||
|
|
||||||
|
## 🛠️ Configuração do Banco de Dados
|
||||||
|
|
||||||
|
### 1. Executar Script SQL
|
||||||
|
|
||||||
|
Acesse seu painel do **Supabase** e execute o script SQL:
|
||||||
|
|
||||||
|
1. **Vá para o Supabase Dashboard**
|
||||||
|
2. **Clique em "SQL Editor"**
|
||||||
|
3. **Cole o conteúdo do arquivo** `sql/emprestimos-tables.sql`
|
||||||
|
4. **Execute o script** clicando em "Run"
|
||||||
|
|
||||||
|
### 2. Verificar Tabelas Criadas
|
||||||
|
|
||||||
|
Após executar, você deve ter as seguintes tabelas:
|
||||||
|
|
||||||
|
- **`emprestimos`** - Registros principais dos empréstimos
|
||||||
|
- **`emprestimo_devolucoes`** - Histórico de devoluções/parcelas
|
||||||
|
|
||||||
|
### 3. Estrutura das Tabelas
|
||||||
|
|
||||||
|
#### Tabela `emprestimos`:
|
||||||
|
```sql
|
||||||
|
- id (UUID) - Chave primária
|
||||||
|
- pessoa (VARCHAR) - Nome da pessoa (padrão: 'Maiara')
|
||||||
|
- valor_total (DECIMAL) - Valor total emprestado
|
||||||
|
- valor_restante (DECIMAL) - Valor ainda não devolvido
|
||||||
|
- descricao (TEXT) - Motivo/descrição do empréstimo
|
||||||
|
- data_emprestimo (DATE) - Data do empréstimo
|
||||||
|
- status (VARCHAR) - ativo, quitado, cancelado
|
||||||
|
- created_at, updated_at (TIMESTAMP)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tabela `emprestimo_devolucoes`:
|
||||||
|
```sql
|
||||||
|
- id (UUID) - Chave primária
|
||||||
|
- emprestimo_id (UUID) - Referência ao empréstimo
|
||||||
|
- valor_devolvido (DECIMAL) - Valor desta devolução
|
||||||
|
- data_devolucao (DATE) - Data da devolução
|
||||||
|
- observacoes (TEXT) - Observações sobre a devolução
|
||||||
|
- created_at, updated_at (TIMESTAMP)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎮 Como Usar o Sistema
|
||||||
|
|
||||||
|
### 1. **Acessar a Página**
|
||||||
|
- No menu lateral, clique em **"Empréstimos"**
|
||||||
|
- Você verá o dashboard com resumos financeiros
|
||||||
|
|
||||||
|
### 2. **Criar Novo Empréstimo**
|
||||||
|
- Clique em **"Novo Empréstimo"**
|
||||||
|
- Preencha os dados:
|
||||||
|
- **Pessoa:** Maiara (padrão)
|
||||||
|
- **Valor Total:** Valor emprestado
|
||||||
|
- **Data:** Data do empréstimo
|
||||||
|
- **Descrição:** Motivo (opcional)
|
||||||
|
- Clique em **"Criar Empréstimo"**
|
||||||
|
|
||||||
|
### 3. **Registrar Devolução**
|
||||||
|
- Na lista de empréstimos, clique no botão **💰** (verde)
|
||||||
|
- Preencha:
|
||||||
|
- **Valor da Devolução:** Qualquer valor até o restante
|
||||||
|
- **Data da Devolução:** Data do pagamento
|
||||||
|
- **Observações:** Detalhes (opcional)
|
||||||
|
- Clique em **"Registrar Devolução"**
|
||||||
|
|
||||||
|
### 4. **Acompanhar Status**
|
||||||
|
- **🟠 Ativo:** Ainda tem valor a receber
|
||||||
|
- **🟢 Quitado:** Totalmente pago
|
||||||
|
- **🔴 Cancelado:** Empréstimo cancelado
|
||||||
|
|
||||||
|
## 📊 Dashboard de Resumos
|
||||||
|
|
||||||
|
O sistema mostra automaticamente:
|
||||||
|
|
||||||
|
- **💰 Total em Aberto:** Soma de todos os valores restantes
|
||||||
|
- **✅ Total Quitado:** Soma de todos os empréstimos pagos
|
||||||
|
- **👤 Empréstimos Ativos:** Quantidade de empréstimos em aberto
|
||||||
|
|
||||||
|
## 🔄 Funcionalidades Automáticas
|
||||||
|
|
||||||
|
### ✨ Cálculos Automáticos:
|
||||||
|
- **Valor restante** é atualizado automaticamente a cada devolução
|
||||||
|
- **Status** muda para "quitado" quando valor restante = 0
|
||||||
|
- **Histórico completo** de todas as movimentações
|
||||||
|
|
||||||
|
### 🛡️ Validações:
|
||||||
|
- Não permite devolver mais que o valor restante
|
||||||
|
- Campos obrigatórios validados
|
||||||
|
- Datas não podem ser futuras (opcional)
|
||||||
|
|
||||||
|
## 🎯 Exemplo de Uso
|
||||||
|
|
||||||
|
### Cenário: Maiara pega R$ 1.000 emprestado
|
||||||
|
|
||||||
|
1. **Criar empréstimo:**
|
||||||
|
- Valor: R$ 1.000,00
|
||||||
|
- Descrição: "Empréstimo para despesas pessoais"
|
||||||
|
|
||||||
|
2. **Primeira devolução:**
|
||||||
|
- Valor: R$ 300,00
|
||||||
|
- Restante: R$ 700,00
|
||||||
|
- Status: Ativo
|
||||||
|
|
||||||
|
3. **Segunda devolução:**
|
||||||
|
- Valor: R$ 700,00
|
||||||
|
- Restante: R$ 0,00
|
||||||
|
- Status: Quitado ✅
|
||||||
|
|
||||||
|
## 🚀 Benefícios do Sistema
|
||||||
|
|
||||||
|
- ✅ **Controle total** do fluxo de caixa
|
||||||
|
- ✅ **Transparência** nas movimentações
|
||||||
|
- ✅ **Histórico completo** para auditoria
|
||||||
|
- ✅ **Interface intuitiva** e fácil de usar
|
||||||
|
- ✅ **Cálculos automáticos** sem erros
|
||||||
|
- ✅ **Relatórios visuais** em tempo real
|
||||||
|
|
||||||
|
## 🔧 Manutenção
|
||||||
|
|
||||||
|
### Backup dos Dados:
|
||||||
|
Os dados ficam seguros no Supabase com backup automático.
|
||||||
|
|
||||||
|
### Relatórios:
|
||||||
|
Todos os dados podem ser exportados via SQL ou integração futura com Google Sheets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🎉 Sistema pronto para uso!**
|
||||||
|
|
||||||
|
Agora você tem controle total sobre os empréstimos da Maiara com uma interface profissional e funcionalidades completas.
|
||||||
123
GOOGLE-DRIVE-FIX.md
Normal file
123
GOOGLE-DRIVE-FIX.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# 🔧 Correção do Erro de Autorização Google Drive
|
||||||
|
|
||||||
|
## Problema Identificado
|
||||||
|
Erro: "Acesso bloqueado: erro de autorização" ao tentar autorizar o Google Drive.
|
||||||
|
|
||||||
|
## Solução Passo a Passo
|
||||||
|
|
||||||
|
### 1. Configurar Google Cloud Console Corretamente
|
||||||
|
|
||||||
|
#### A. Criar/Verificar Projeto
|
||||||
|
1. Acesse [Google Cloud Console](https://console.cloud.google.com/)
|
||||||
|
2. Selecione ou crie um projeto
|
||||||
|
3. Nome sugerido: `Liberi Kids Sistema`
|
||||||
|
|
||||||
|
#### B. Ativar APIs Necessárias
|
||||||
|
1. Vá em **"APIs e serviços"** → **"Biblioteca"**
|
||||||
|
2. Pesquise e ative:
|
||||||
|
- **Google Drive API**
|
||||||
|
- **Google Sheets API** (se usar)
|
||||||
|
3. Clique em **"Ativar"** para cada uma
|
||||||
|
|
||||||
|
#### C. Configurar Tela de Consentimento OAuth
|
||||||
|
1. Vá em **"APIs e serviços"** → **"Tela de consentimento OAuth"**
|
||||||
|
2. Selecione **"Externo"** (para uso pessoal/pequenas empresas)
|
||||||
|
3. Preencha os campos obrigatórios:
|
||||||
|
- **Nome do app:** `Liberi Kids - Sistema de Estoque`
|
||||||
|
- **Email de suporte:** Seu email
|
||||||
|
- **Domínios autorizados:** `localhost` (adicionar se necessário)
|
||||||
|
- **Email do desenvolvedor:** Seu email
|
||||||
|
4. Clique em **"Salvar e continuar"**
|
||||||
|
|
||||||
|
#### D. Adicionar Escopos
|
||||||
|
1. Na seção **"Escopos"**, clique em **"Adicionar ou remover escopos"**
|
||||||
|
2. Adicione os escopos:
|
||||||
|
```
|
||||||
|
https://www.googleapis.com/auth/drive.file
|
||||||
|
https://www.googleapis.com/auth/drive.readonly
|
||||||
|
```
|
||||||
|
3. Clique em **"Atualizar"** e **"Salvar e continuar"**
|
||||||
|
|
||||||
|
#### E. Adicionar Usuários de Teste (IMPORTANTE)
|
||||||
|
1. Na seção **"Usuários de teste"**
|
||||||
|
2. Clique em **"+ Adicionar usuários"**
|
||||||
|
3. Adicione seu email (o mesmo que vai usar para autorizar)
|
||||||
|
4. Clique em **"Salvar e continuar"**
|
||||||
|
|
||||||
|
### 2. Criar Credenciais OAuth 2.0
|
||||||
|
|
||||||
|
1. Vá em **"APIs e serviços"** → **"Credenciais"**
|
||||||
|
2. Clique em **"+ Criar credenciais"** → **"ID do cliente OAuth"**
|
||||||
|
3. Selecione **"Aplicação da Web"**
|
||||||
|
4. Configure:
|
||||||
|
- **Nome:** `Liberi Kids Drive Integration`
|
||||||
|
- **URIs de origem autorizados:**
|
||||||
|
```
|
||||||
|
http://localhost:5000
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
- **URIs de redirecionamento autorizados:**
|
||||||
|
```
|
||||||
|
http://localhost:5000/auth/google-drive/callback
|
||||||
|
```
|
||||||
|
5. Clique em **"Criar"**
|
||||||
|
6. **Copie o Client ID e Client Secret**
|
||||||
|
|
||||||
|
### 3. Configurar no Sistema
|
||||||
|
|
||||||
|
1. Acesse **Configurações** no sistema Liberi Kids
|
||||||
|
2. Vá na seção **"📁 Google Drive - Armazenamento de Fotos"**
|
||||||
|
3. Cole as credenciais:
|
||||||
|
- **Client ID:** Cole o ID copiado
|
||||||
|
- **Client Secret:** Cole o Secret copiado
|
||||||
|
4. Clique em **"Salvar Credenciais"**
|
||||||
|
5. Clique em **"Autorizar Google Drive"**
|
||||||
|
|
||||||
|
### 4. Autorização Correta
|
||||||
|
|
||||||
|
1. Uma nova janela abrirá com o Google
|
||||||
|
2. **Faça login com o email que você adicionou como usuário de teste**
|
||||||
|
3. Você verá um aviso: "Google hasn't verified this app"
|
||||||
|
4. Clique em **"Advanced"** (Avançado)
|
||||||
|
5. Clique em **"Go to Liberi Kids Sistema (unsafe)"**
|
||||||
|
6. Autorize as permissões solicitadas
|
||||||
|
7. A janela fechará automaticamente
|
||||||
|
|
||||||
|
## Dicas Importantes
|
||||||
|
|
||||||
|
### ✅ Checklist de Verificação
|
||||||
|
- [ ] Google Drive API ativada
|
||||||
|
- [ ] Tela de consentimento configurada
|
||||||
|
- [ ] Email adicionado como usuário de teste
|
||||||
|
- [ ] URIs de redirecionamento corretos
|
||||||
|
- [ ] Credenciais copiadas corretamente
|
||||||
|
|
||||||
|
### 🔒 Segurança
|
||||||
|
- Use apenas para desenvolvimento/uso pessoal
|
||||||
|
- Para produção, considere verificar a aplicação com Google
|
||||||
|
- Mantenha as credenciais seguras
|
||||||
|
|
||||||
|
### 🚨 Problemas Comuns
|
||||||
|
1. **"Erro 400: redirect_uri_mismatch"**
|
||||||
|
- Verifique se o URI está exatamente igual no Google Cloud
|
||||||
|
|
||||||
|
2. **"Acesso negado"**
|
||||||
|
- Certifique-se de estar logado com o email de teste
|
||||||
|
|
||||||
|
3. **"App não verificado"**
|
||||||
|
- Normal para desenvolvimento, clique em "Avançado"
|
||||||
|
|
||||||
|
## Testando a Configuração
|
||||||
|
|
||||||
|
Após configurar:
|
||||||
|
1. Vá em **Produtos** → **Novo Produto**
|
||||||
|
2. Adicione fotos ao produto
|
||||||
|
3. Verifique se aparecem mensagens de upload para Google Drive
|
||||||
|
4. Confirme se as fotos aparecem no seu Google Drive
|
||||||
|
|
||||||
|
## Suporte
|
||||||
|
|
||||||
|
Se ainda houver problemas:
|
||||||
|
1. Verifique os logs do servidor
|
||||||
|
2. Confirme se todas as APIs estão ativas
|
||||||
|
3. Verifique se o email de teste está correto
|
||||||
146
GOOGLE-DRIVE-SETUP.md
Normal file
146
GOOGLE-DRIVE-SETUP.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# 📁 Google Drive Integration - Guia de Configuração
|
||||||
|
|
||||||
|
Este guia explica como configurar a integração com Google Drive para armazenar as fotos dos produtos automaticamente na nuvem.
|
||||||
|
|
||||||
|
## 🎯 Benefícios da Integração
|
||||||
|
|
||||||
|
- **☁️ Armazenamento na Nuvem:** Fotos salvas automaticamente no Google Drive
|
||||||
|
- **🔒 Backup Seguro:** Suas imagens ficam protegidas na nuvem do Google
|
||||||
|
- **📱 Acesso Universal:** Visualize as fotos de qualquer dispositivo
|
||||||
|
- **💾 Economia de Espaço:** Não ocupa espaço no servidor local
|
||||||
|
- **🔗 URLs Públicas:** Links diretos para visualização das imagens
|
||||||
|
|
||||||
|
## 🚀 Configuração Passo a Passo
|
||||||
|
|
||||||
|
### 1. Criar Projeto no Google Cloud Console
|
||||||
|
|
||||||
|
1. Acesse [Google Cloud Console](https://console.cloud.google.com/)
|
||||||
|
2. Clique em **"Selecionar projeto"** → **"Novo projeto"**
|
||||||
|
3. Digite o nome: `Liberi Kids - Sistema`
|
||||||
|
4. Clique em **"Criar"**
|
||||||
|
|
||||||
|
### 2. Ativar Google Drive API
|
||||||
|
|
||||||
|
1. No menu lateral, vá em **"APIs e serviços"** → **"Biblioteca"**
|
||||||
|
2. Pesquise por **"Google Drive API"**
|
||||||
|
3. Clique na API e depois em **"Ativar"**
|
||||||
|
|
||||||
|
### 3. Criar Credenciais OAuth 2.0
|
||||||
|
|
||||||
|
1. Vá em **"APIs e serviços"** → **"Credenciais"**
|
||||||
|
2. Clique em **"+ Criar credenciais"** → **"ID do cliente OAuth"**
|
||||||
|
3. Selecione **"Aplicação da Web"**
|
||||||
|
4. Configure:
|
||||||
|
- **Nome:** `Liberi Kids Sistema`
|
||||||
|
- **URIs de redirecionamento autorizados:**
|
||||||
|
```
|
||||||
|
http://localhost:5000/auth/google-drive/callback
|
||||||
|
```
|
||||||
|
5. Clique em **"Criar"**
|
||||||
|
6. **Copie o Client ID e Client Secret** que aparecerão
|
||||||
|
|
||||||
|
### 4. Configurar no Sistema
|
||||||
|
|
||||||
|
1. Acesse **Configurações** no sistema Liberi Kids
|
||||||
|
2. Encontre a seção **"📁 Google Drive - Armazenamento de Fotos"**
|
||||||
|
3. Clique para expandir
|
||||||
|
4. Cole as credenciais:
|
||||||
|
- **Client ID:** Cole o ID copiado do Google Cloud
|
||||||
|
- **Client Secret:** Cole o Secret copiado do Google Cloud
|
||||||
|
5. Clique em **"Salvar Credenciais"**
|
||||||
|
|
||||||
|
### 5. Autorizar Acesso
|
||||||
|
|
||||||
|
1. Após salvar, clique em **"Autorizar Google Drive"**
|
||||||
|
2. Uma nova janela abrirá com a tela de login do Google
|
||||||
|
3. Faça login com sua conta Google
|
||||||
|
4. Autorize o acesso aos arquivos do Google Drive
|
||||||
|
5. A janela fechará automaticamente
|
||||||
|
6. Verifique se o status mudou para **"✅ Conectado"**
|
||||||
|
|
||||||
|
## 📋 Como Funciona
|
||||||
|
|
||||||
|
### Upload Automático
|
||||||
|
- Quando você cadastra um produto com fotos, o sistema detecta automaticamente se o Google Drive está configurado
|
||||||
|
- Se estiver conectado, as fotos são enviadas para a pasta **"Liberi Kids - Fotos Produtos"**
|
||||||
|
- Caso contrário, usa o armazenamento local como antes
|
||||||
|
|
||||||
|
### Organização das Fotos
|
||||||
|
- **Pasta Principal:** `Liberi Kids - Fotos Produtos`
|
||||||
|
- **Nome dos Arquivos:** `Marca_NomeProduto_Tamanho_Cor_Timestamp.jpg`
|
||||||
|
- **Exemplo:** `Nike_Camiseta_M_Azul_1641234567890.jpg`
|
||||||
|
|
||||||
|
### URLs Públicas
|
||||||
|
- Cada foto recebe uma URL pública do Google Drive
|
||||||
|
- As URLs são salvas no banco de dados
|
||||||
|
- As imagens são exibidas normalmente no sistema
|
||||||
|
|
||||||
|
## 🔧 Configurações Avançadas
|
||||||
|
|
||||||
|
### Informações de Armazenamento
|
||||||
|
O sistema mostra:
|
||||||
|
- **Espaço Usado:** Quanto você já utilizou
|
||||||
|
- **Espaço Total:** Limite da sua conta Google
|
||||||
|
- **Espaço Livre:** Quanto ainda pode usar
|
||||||
|
|
||||||
|
### Renovação Automática
|
||||||
|
- Os tokens de acesso são renovados automaticamente
|
||||||
|
- Não é necessário reautorizar frequentemente
|
||||||
|
- O sistema mantém a conexão ativa
|
||||||
|
|
||||||
|
## 🛠️ Solução de Problemas
|
||||||
|
|
||||||
|
### Erro: "Credenciais não configuradas"
|
||||||
|
- Verifique se copiou corretamente o Client ID e Client Secret
|
||||||
|
- Certifique-se de que ativou a Google Drive API
|
||||||
|
|
||||||
|
### Erro: "Autorização pendente"
|
||||||
|
- Clique em "Autorizar Google Drive" novamente
|
||||||
|
- Verifique se não bloqueou pop-ups no navegador
|
||||||
|
|
||||||
|
### Erro: "Erro de conexão"
|
||||||
|
- Clique em "Tentar Novamente"
|
||||||
|
- Se persistir, clique em "Reconfigurar Tudo"
|
||||||
|
|
||||||
|
### Fotos não aparecem
|
||||||
|
- Verifique se as URLs começam com `https://drive.google.com/`
|
||||||
|
- Teste se consegue acessar a pasta no Google Drive
|
||||||
|
|
||||||
|
## 📊 Monitoramento
|
||||||
|
|
||||||
|
### Status da Conexão
|
||||||
|
- **🟢 Conectado:** Tudo funcionando
|
||||||
|
- **🟡 Não configurado:** Precisa configurar credenciais
|
||||||
|
- **🔵 Autorização pendente:** Precisa autorizar acesso
|
||||||
|
- **🔴 Erro de conexão:** Problema na conexão
|
||||||
|
|
||||||
|
### Ações Disponíveis
|
||||||
|
- **Atualizar Status:** Verifica a conexão atual
|
||||||
|
- **Abrir Google Drive:** Acessa sua pasta no navegador
|
||||||
|
- **Desconectar:** Remove a integração
|
||||||
|
|
||||||
|
## 🔐 Segurança
|
||||||
|
|
||||||
|
### Dados Protegidos
|
||||||
|
- Credenciais salvas criptografadas no Supabase
|
||||||
|
- Tokens renovados automaticamente
|
||||||
|
- Acesso limitado apenas às pastas necessárias
|
||||||
|
|
||||||
|
### Permissões
|
||||||
|
O sistema solicita apenas:
|
||||||
|
- **drive.file:** Criar e gerenciar arquivos criados pelo app
|
||||||
|
- **drive.readonly:** Ler informações básicas do Drive
|
||||||
|
|
||||||
|
## 📝 Notas Importantes
|
||||||
|
|
||||||
|
1. **Conta Google:** Use uma conta Google com espaço suficiente
|
||||||
|
2. **Backup Local:** O sistema mantém fallback para armazenamento local
|
||||||
|
3. **Compatibilidade:** Funciona com produtos existentes
|
||||||
|
4. **Performance:** Upload pode ser mais lento que armazenamento local
|
||||||
|
5. **Dependência:** Requer conexão com internet para upload
|
||||||
|
|
||||||
|
## 🎉 Pronto!
|
||||||
|
|
||||||
|
Após seguir todos os passos, suas fotos de produtos serão automaticamente salvas no Google Drive, proporcionando backup seguro e acesso universal às imagens do seu estoque.
|
||||||
|
|
||||||
|
Para dúvidas ou problemas, verifique os logs do sistema ou entre em contato com o suporte técnico.
|
||||||
148
GOOGLE-SHEETS-SETUP.md
Normal file
148
GOOGLE-SHEETS-SETUP.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# 📊 Configuração Google Sheets - Liberi Kids
|
||||||
|
|
||||||
|
Este guia explica como configurar a integração com Google Sheets para exportar dados do sistema.
|
||||||
|
|
||||||
|
## 🚀 Passo a Passo
|
||||||
|
|
||||||
|
### 1. Criar Projeto no Google Cloud Console
|
||||||
|
|
||||||
|
1. Acesse [Google Cloud Console](https://console.cloud.google.com/)
|
||||||
|
2. Clique em **"Criar Projeto"** ou selecione um existente
|
||||||
|
3. Dê um nome ao projeto (ex: "Liberi Kids Sheets")
|
||||||
|
4. Anote o **Project ID** gerado
|
||||||
|
|
||||||
|
### 2. Ativar APIs Necessárias
|
||||||
|
|
||||||
|
1. No menu lateral, vá em **"APIs e Serviços" > "Biblioteca"**
|
||||||
|
2. Procure e ative as seguintes APIs:
|
||||||
|
- **Google Sheets API**
|
||||||
|
- **Google Drive API**
|
||||||
|
|
||||||
|
### 3. Criar Credenciais OAuth 2.0
|
||||||
|
|
||||||
|
1. Vá em **"APIs e Serviços" > "Credenciais"**
|
||||||
|
2. Clique em **"+ CRIAR CREDENCIAIS" > "ID do cliente OAuth"**
|
||||||
|
3. Escolha **"Aplicação da Web"**
|
||||||
|
4. Configure:
|
||||||
|
- **Nome:** Liberi Kids
|
||||||
|
- **URIs de redirecionamento autorizados:**
|
||||||
|
```
|
||||||
|
http://localhost:5000/auth/google/callback
|
||||||
|
```
|
||||||
|
- Para produção, adicione também:
|
||||||
|
```
|
||||||
|
https://seu-dominio.com/auth/google/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Copiar Credenciais
|
||||||
|
|
||||||
|
1. Após criar, você verá as credenciais na tela
|
||||||
|
2. **Copie o Client ID** (formato: 123456789-abc123.apps.googleusercontent.com)
|
||||||
|
3. **Copie o Client Secret** (formato: GOCSPX-abc123def456...)
|
||||||
|
4. Guarde essas informações para inserir na interface do sistema
|
||||||
|
|
||||||
|
### 5. Configurar Tela de Consentimento OAuth
|
||||||
|
|
||||||
|
1. Vá em **"APIs e Serviços" > "Tela de consentimento OAuth"**
|
||||||
|
2. Escolha **"Externo"** (para uso geral)
|
||||||
|
3. Preencha as informações obrigatórias:
|
||||||
|
- **Nome do app:** Liberi Kids
|
||||||
|
- **Email de suporte:** seu-email@exemplo.com
|
||||||
|
- **Domínios autorizados:** localhost (para desenvolvimento)
|
||||||
|
|
||||||
|
### 6. Adicionar Escopos
|
||||||
|
|
||||||
|
Na configuração da tela de consentimento, adicione os escopos:
|
||||||
|
- `https://www.googleapis.com/auth/spreadsheets`
|
||||||
|
- `https://www.googleapis.com/auth/drive.file`
|
||||||
|
|
||||||
|
## 🔧 Configuração no Sistema
|
||||||
|
|
||||||
|
Após obter as credenciais do Google Cloud Console:
|
||||||
|
|
||||||
|
1. **Acesse o sistema Liberi Kids**
|
||||||
|
2. **Vá em Configurações > Google Sheets**
|
||||||
|
3. **Preencha os campos:**
|
||||||
|
- **Client ID:** Cole o Client ID copiado
|
||||||
|
- **Client Secret:** Cole o Client Secret copiado
|
||||||
|
- **URI de Redirecionamento:** Já preenchido automaticamente
|
||||||
|
4. **Clique em "Salvar Credenciais"**
|
||||||
|
5. **Clique em "Conectar com Google"** para autorizar
|
||||||
|
|
||||||
|
## 📋 Dados Exportados
|
||||||
|
|
||||||
|
### Aba "Produtos"
|
||||||
|
- ID da Roupa
|
||||||
|
- Nome do Produto
|
||||||
|
- Fornecedor
|
||||||
|
- Tamanho
|
||||||
|
- Estação
|
||||||
|
- Gênero
|
||||||
|
- Valor da Compra
|
||||||
|
- Valor da Venda
|
||||||
|
- Data da Compra
|
||||||
|
- Data da Venda
|
||||||
|
- Estoque Atual
|
||||||
|
- Marca
|
||||||
|
|
||||||
|
### Aba "Vendas"
|
||||||
|
- ID da Venda
|
||||||
|
- Cliente
|
||||||
|
- Data da Venda
|
||||||
|
- Tipo de Pagamento
|
||||||
|
- Valor Total
|
||||||
|
- Desconto
|
||||||
|
- Valor Final
|
||||||
|
- Status
|
||||||
|
- Observações
|
||||||
|
- Produtos Vendidos
|
||||||
|
|
||||||
|
## 🎯 Como Usar
|
||||||
|
|
||||||
|
1. **Configuração inicial:**
|
||||||
|
- Configure as credenciais conforme descrito acima
|
||||||
|
- Autorize o acesso ao Google na primeira vez
|
||||||
|
|
||||||
|
2. **Exportar dados:**
|
||||||
|
- Vá em Configurações > Google Sheets
|
||||||
|
- Escolha o tipo de exportação (Produtos, Vendas ou Tudo)
|
||||||
|
- Defina um nome para a planilha (opcional)
|
||||||
|
- Clique em exportar
|
||||||
|
- A planilha será criada e aberta automaticamente
|
||||||
|
|
||||||
|
## 🔒 Segurança
|
||||||
|
|
||||||
|
- ✅ **Credenciais salvas no Supabase** de forma segura
|
||||||
|
- ✅ **Tokens são salvos localmente** e renovados automaticamente
|
||||||
|
- ✅ **Acesso limitado** apenas às planilhas criadas pelo app
|
||||||
|
- ✅ **Dados criptografados** durante a transmissão
|
||||||
|
|
||||||
|
## 🚨 Troubleshooting
|
||||||
|
|
||||||
|
### Erro: "Credenciais do Google não configuradas"
|
||||||
|
- Vá em Configurações > Google Sheets
|
||||||
|
- Preencha os campos Client ID e Client Secret
|
||||||
|
- Clique em "Salvar Credenciais"
|
||||||
|
|
||||||
|
### Erro: "redirect_uri_mismatch"
|
||||||
|
- Verifique se o URI de redirecionamento está correto no Google Cloud Console
|
||||||
|
- Para desenvolvimento: `http://localhost:5000/auth/google/callback`
|
||||||
|
|
||||||
|
### Erro: "access_denied"
|
||||||
|
- Verifique se as APIs estão ativadas
|
||||||
|
- Confirme se os escopos estão configurados corretamente
|
||||||
|
|
||||||
|
### Planilha não abre automaticamente
|
||||||
|
- Verifique se o bloqueador de pop-ups está desabilitado
|
||||||
|
- A URL da planilha aparece no toast de sucesso
|
||||||
|
|
||||||
|
## 📞 Suporte
|
||||||
|
|
||||||
|
Se encontrar problemas:
|
||||||
|
1. Verifique os logs do servidor no terminal
|
||||||
|
2. Confirme se todas as APIs estão ativadas
|
||||||
|
3. Teste a conexão na página de Configurações
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Desenvolvido para Liberi Kids - Sistema de Controle de Estoque**
|
||||||
387
INTEGRACAO-PIX-GUIDE.md
Normal file
387
INTEGRACAO-PIX-GUIDE.md
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
# 🏦 Integração PIX com QR Code - Liberi Kids
|
||||||
|
|
||||||
|
## 🎯 **Opções de Integração PIX**
|
||||||
|
|
||||||
|
### **1. Mercado Pago (Recomendado)**
|
||||||
|
- ✅ **Gratuito** para começar
|
||||||
|
- ✅ **QR Code automático**
|
||||||
|
- ✅ **Webhook para confirmação**
|
||||||
|
- ✅ **Documentação excelente**
|
||||||
|
|
||||||
|
### **2. PagSeguro/PagBank**
|
||||||
|
- ✅ **PIX instantâneo**
|
||||||
|
- ✅ **Taxas competitivas**
|
||||||
|
|
||||||
|
### **3. Asaas**
|
||||||
|
- ✅ **Focado em pequenas empresas**
|
||||||
|
- ✅ **PIX + Boleto + Cartão**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 **Implementação Mercado Pago**
|
||||||
|
|
||||||
|
### **Passo 1: Criar Conta**
|
||||||
|
1. Acesse: https://www.mercadopago.com.br/developers
|
||||||
|
2. Crie conta de desenvolvedor
|
||||||
|
3. Obtenha suas credenciais:
|
||||||
|
- **Public Key** (pk_test_...)
|
||||||
|
- **Access Token** (TEST-...)
|
||||||
|
|
||||||
|
### **Passo 2: Instalar SDK**
|
||||||
|
```bash
|
||||||
|
npm install mercadopago
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Passo 3: Configurar no .env**
|
||||||
|
```env
|
||||||
|
# Mercado Pago PIX
|
||||||
|
MERCADOPAGO_ACCESS_TOKEN=TEST-sua_access_token_aqui
|
||||||
|
MERCADOPAGO_PUBLIC_KEY=pk_test_sua_public_key_aqui
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Passo 4: API Backend**
|
||||||
|
```javascript
|
||||||
|
// server-supabase.js - Adicionar rota PIX
|
||||||
|
|
||||||
|
const mercadopago = require('mercadopago');
|
||||||
|
|
||||||
|
// Configurar Mercado Pago
|
||||||
|
mercadopago.configurations.setAccessToken(process.env.MERCADOPAGO_ACCESS_TOKEN);
|
||||||
|
|
||||||
|
// Rota para gerar PIX
|
||||||
|
app.post('/api/pix/gerar', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { valor, descricao, cliente_email, venda_id } = req.body;
|
||||||
|
|
||||||
|
const payment_data = {
|
||||||
|
transaction_amount: parseFloat(valor),
|
||||||
|
description: descricao || `Venda #${venda_id} - Liberi Kids`,
|
||||||
|
payment_method_id: 'pix',
|
||||||
|
payer: {
|
||||||
|
email: cliente_email || 'cliente@liberikids.com'
|
||||||
|
},
|
||||||
|
external_reference: venda_id.toString(),
|
||||||
|
notification_url: `${process.env.BASE_URL}/api/pix/webhook`
|
||||||
|
};
|
||||||
|
|
||||||
|
const payment = await mercadopago.payment.create(payment_data);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
payment_id: payment.body.id,
|
||||||
|
qr_code: payment.body.point_of_interaction.transaction_data.qr_code,
|
||||||
|
qr_code_base64: payment.body.point_of_interaction.transaction_data.qr_code_base64,
|
||||||
|
pix_copy_paste: payment.body.point_of_interaction.transaction_data.qr_code,
|
||||||
|
expiration_date: payment.body.date_of_expiration
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao gerar PIX:', error);
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Webhook para receber confirmação de pagamento
|
||||||
|
app.post('/api/pix/webhook', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { type, data } = req.body;
|
||||||
|
|
||||||
|
if (type === 'payment') {
|
||||||
|
const payment = await mercadopago.payment.get(data.id);
|
||||||
|
|
||||||
|
if (payment.body.status === 'approved') {
|
||||||
|
const venda_id = payment.body.external_reference;
|
||||||
|
|
||||||
|
// Atualizar status da venda no banco
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('vendas')
|
||||||
|
.update({
|
||||||
|
status_pagamento: 'pago',
|
||||||
|
data_pagamento: new Date(),
|
||||||
|
pix_payment_id: payment.body.id
|
||||||
|
})
|
||||||
|
.eq('id', venda_id);
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
console.log(`Pagamento confirmado para venda #${venda_id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send('OK');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro no webhook PIX:', error);
|
||||||
|
res.status(500).send('Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Passo 5: Frontend - Modal PIX**
|
||||||
|
```javascript
|
||||||
|
// Adicionar ao Vendas.js
|
||||||
|
|
||||||
|
const [showPixModal, setShowPixModal] = useState(false);
|
||||||
|
const [pixData, setPixData] = useState(null);
|
||||||
|
const [loadingPix, setLoadingPix] = useState(false);
|
||||||
|
|
||||||
|
const gerarPix = async (venda) => {
|
||||||
|
setLoadingPix(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/pix/gerar', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
valor: venda.valor_total,
|
||||||
|
descricao: `Venda #${venda.id_venda} - Liberi Kids`,
|
||||||
|
cliente_email: venda.cliente?.email,
|
||||||
|
venda_id: venda.id
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setPixData(data);
|
||||||
|
setShowPixModal(true);
|
||||||
|
} else {
|
||||||
|
toast.error('Erro ao gerar PIX');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Erro ao gerar PIX');
|
||||||
|
}
|
||||||
|
setLoadingPix(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modal PIX
|
||||||
|
{showPixModal && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal pix-modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h3>🏦 Pagamento PIX</h3>
|
||||||
|
<button onClick={() => setShowPixModal(false)}>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="pix-content">
|
||||||
|
<div className="qr-code-section">
|
||||||
|
<h4>QR Code PIX</h4>
|
||||||
|
<img
|
||||||
|
src={`data:image/png;base64,${pixData.qr_code_base64}`}
|
||||||
|
alt="QR Code PIX"
|
||||||
|
className="qr-code-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pix-copy-section">
|
||||||
|
<h4>Código PIX (Copiar e Colar)</h4>
|
||||||
|
<div className="pix-code-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={pixData.pix_copy_paste}
|
||||||
|
readOnly
|
||||||
|
className="pix-code-input"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(pixData.pix_copy_paste);
|
||||||
|
toast.success('Código PIX copiado!');
|
||||||
|
}}
|
||||||
|
className="btn-copy-pix"
|
||||||
|
>
|
||||||
|
📋 Copiar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pix-info">
|
||||||
|
<p><strong>Valor:</strong> R$ {parseFloat(pixData.transaction_amount).toFixed(2)}</p>
|
||||||
|
<p><strong>Válido até:</strong> {new Date(pixData.expiration_date).toLocaleString()}</p>
|
||||||
|
<p className="pix-instructions">
|
||||||
|
📱 <strong>Como pagar:</strong><br/>
|
||||||
|
1. Abra o app do seu banco<br/>
|
||||||
|
2. Escaneie o QR Code ou cole o código PIX<br/>
|
||||||
|
3. Confirme o pagamento
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Passo 6: CSS para Modal PIX**
|
||||||
|
```css
|
||||||
|
/* styles/pix-integration.css */
|
||||||
|
|
||||||
|
.pix-modal {
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-image {
|
||||||
|
max-width: 200px;
|
||||||
|
height: auto;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-copy-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-code-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-code-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy-pix {
|
||||||
|
background: #00d4aa;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy-pix:hover {
|
||||||
|
background: #00b894;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-info {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-instructions {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pix {
|
||||||
|
background: #00d4aa;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pix:hover {
|
||||||
|
background: #00b894;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **Integração no Sistema de Vendas**
|
||||||
|
|
||||||
|
### **1. Adicionar Botão PIX na Lista de Vendas**
|
||||||
|
```javascript
|
||||||
|
// Na tabela de vendas, adicionar botão PIX
|
||||||
|
<button
|
||||||
|
onClick={() => gerarPix(venda)}
|
||||||
|
className="btn-icon btn-pix"
|
||||||
|
title="Gerar PIX"
|
||||||
|
disabled={loadingPix}
|
||||||
|
>
|
||||||
|
🏦 PIX
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Adicionar Campo no Banco de Dados**
|
||||||
|
```sql
|
||||||
|
-- Adicionar colunas para controle de PIX
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS status_pagamento VARCHAR(20) DEFAULT 'pendente';
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS data_pagamento TIMESTAMP;
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS pix_payment_id VARCHAR(100);
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. Dashboard com Status de Pagamentos**
|
||||||
|
- Vendas pendentes de pagamento
|
||||||
|
- Vendas pagas via PIX
|
||||||
|
- Relatório de recebimentos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 **Custos e Taxas**
|
||||||
|
|
||||||
|
### **Mercado Pago:**
|
||||||
|
- **PIX:** 0,99% por transação
|
||||||
|
- **Sem mensalidade**
|
||||||
|
- **Recebimento em 1 dia útil**
|
||||||
|
|
||||||
|
### **PagSeguro:**
|
||||||
|
- **PIX:** 0,99% por transação
|
||||||
|
- **Recebimento em 1 dia útil**
|
||||||
|
|
||||||
|
### **Asaas:**
|
||||||
|
- **PIX:** R$ 0,20 por transação
|
||||||
|
- **Melhor para alto volume**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 **Segurança**
|
||||||
|
|
||||||
|
1. **Nunca exponha** as credenciais no frontend
|
||||||
|
2. **Use HTTPS** sempre em produção
|
||||||
|
3. **Valide webhooks** com assinatura
|
||||||
|
4. **Monitore transações** suspeitas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 **Funcionalidades Extras**
|
||||||
|
|
||||||
|
### **1. WhatsApp com PIX**
|
||||||
|
- Enviar QR Code via WhatsApp
|
||||||
|
- Link de pagamento direto
|
||||||
|
|
||||||
|
### **2. Relatórios PIX**
|
||||||
|
- Vendas pagas via PIX
|
||||||
|
- Tempo médio de pagamento
|
||||||
|
- Conversão PIX vs outros métodos
|
||||||
|
|
||||||
|
### **3. Notificações**
|
||||||
|
- Email quando PIX for pago
|
||||||
|
- Notificação no sistema
|
||||||
|
- Atualização automática do status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 **Benefícios da Integração**
|
||||||
|
|
||||||
|
- ✅ **Pagamento instantâneo**
|
||||||
|
- ✅ **Sem taxas para o cliente**
|
||||||
|
- ✅ **QR Code automático**
|
||||||
|
- ✅ **Confirmação automática**
|
||||||
|
- ✅ **Integração com WhatsApp**
|
||||||
|
- ✅ **Relatórios detalhados**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🚀 Com essa integração, seu sistema Liberi Kids terá pagamentos PIX completos com QR Code automático!**
|
||||||
236
INTERFACE-TROCAS-CORRIGIDA.md
Normal file
236
INTERFACE-TROCAS-CORRIGIDA.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# ✅ Interface de Trocas/Devoluções Corrigida
|
||||||
|
|
||||||
|
## 🔧 Problemas Identificados e Solucionados
|
||||||
|
|
||||||
|
### **Problema Original:**
|
||||||
|
- Modal de visualização não mostrava informações de troca
|
||||||
|
- Produtos cortados quando havia mais de 2 itens
|
||||||
|
- Status não indicava que houve troca
|
||||||
|
- Falta de histórico de devoluções/trocas
|
||||||
|
|
||||||
|
### **Soluções Implementadas:**
|
||||||
|
|
||||||
|
## 1. **API Backend Aprimorada**
|
||||||
|
|
||||||
|
### **Busca de Vendas com Informações de Troca:**
|
||||||
|
```javascript
|
||||||
|
// Nova funcionalidade na API GET /api/vendas/:id
|
||||||
|
const { data: devolucoes, error: devolucaoError } = await supabase
|
||||||
|
.from('devolucoes')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
venda_itens(
|
||||||
|
produtos(nome, marca),
|
||||||
|
produto_variacoes(tamanho, cor)
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
.eq('venda_id', id)
|
||||||
|
.order('data_devolucao', { ascending: false });
|
||||||
|
|
||||||
|
// Verificar se há trocas
|
||||||
|
const temTrocas = devolucoes?.some(dev => dev.tipo_operacao === 'troca') || false;
|
||||||
|
const statusVenda = temTrocas ? 'com_troca' : (data.status || 'concluida');
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Dados Retornados:**
|
||||||
|
- ✅ **status:** 'com_troca' quando há trocas
|
||||||
|
- ✅ **tem_trocas:** boolean indicando se há trocas
|
||||||
|
- ✅ **devolucoes:** array com histórico completo
|
||||||
|
- ✅ **itens:** todos os itens incluindo trocados
|
||||||
|
|
||||||
|
## 2. **Interface Frontend Melhorada**
|
||||||
|
|
||||||
|
### **Status Visual Atualizado:**
|
||||||
|
```javascript
|
||||||
|
// Novo badge para vendas com troca
|
||||||
|
<span className={`badge ${
|
||||||
|
selectedVenda.status === 'com_troca' ? 'badge-warning' :
|
||||||
|
(selectedVenda.status || 'concluida') === 'concluida' ? 'badge-success' : 'badge-danger'
|
||||||
|
}`}>
|
||||||
|
{selectedVenda.status === 'com_troca' ? 'Com Troca' :
|
||||||
|
(selectedVenda.status || 'concluida') === 'concluida' ? 'Concluída' : 'Cancelada'}
|
||||||
|
</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Nova Seção: Histórico de Devoluções/Trocas**
|
||||||
|
```javascript
|
||||||
|
{selectedVenda.devolucoes && selectedVenda.devolucoes.length > 0 && (
|
||||||
|
<div className="view-section">
|
||||||
|
<h3>Histórico de Devoluções/Trocas</h3>
|
||||||
|
<div className="devolucoes-view">
|
||||||
|
{selectedVenda.devolucoes.map((devolucao, index) => (
|
||||||
|
<div key={index} className="devolucao-item">
|
||||||
|
<div className="devolucao-header">
|
||||||
|
<span className={`badge ${devolucao.tipo_operacao === 'troca' ? 'badge-warning' : 'badge-info'}`}>
|
||||||
|
{devolucao.tipo_operacao === 'troca' ? 'Troca' : 'Devolução'}
|
||||||
|
</span>
|
||||||
|
<span className="devolucao-data">
|
||||||
|
{new Date(devolucao.data_devolucao).toLocaleDateString('pt-BR')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="devolucao-details">
|
||||||
|
<div>Quantidade: {devolucao.quantidade_devolvida}</div>
|
||||||
|
<div>Valor: R$ {parseFloat(devolucao.valor_devolucao).toFixed(2)}</div>
|
||||||
|
{devolucao.motivo && <div>Motivo: {devolucao.motivo}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Seção de Itens Aprimorada:**
|
||||||
|
```javascript
|
||||||
|
<h3>Itens da Venda {selectedVenda.tem_trocas ? '(Incluindo Trocas)' : ''}</h3>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. **Estilos CSS Implementados**
|
||||||
|
|
||||||
|
### **Modal Expandido:**
|
||||||
|
```css
|
||||||
|
.modal-lg {
|
||||||
|
max-width: 900px !important;
|
||||||
|
width: 95% !important;
|
||||||
|
max-height: 90vh !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Seção de Devoluções:**
|
||||||
|
```css
|
||||||
|
.devolucoes-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devolucao-item {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devolucao-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Scroll para Muitos Itens:**
|
||||||
|
```css
|
||||||
|
.items-view {
|
||||||
|
max-height: 400px !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Badge de Troca:**
|
||||||
|
```css
|
||||||
|
.badge-warning {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. **Funcionalidades Implementadas**
|
||||||
|
|
||||||
|
### **✅ Status Correto:**
|
||||||
|
- **"Concluída"** - Venda normal sem alterações
|
||||||
|
- **"Com Troca"** - Venda que teve produtos trocados
|
||||||
|
- **"Cancelada"** - Venda cancelada
|
||||||
|
|
||||||
|
### **✅ Histórico Completo:**
|
||||||
|
- Data da devolução/troca
|
||||||
|
- Tipo de operação (Devolução ou Troca)
|
||||||
|
- Quantidade devolvida
|
||||||
|
- Valor da operação
|
||||||
|
- Motivo (quando informado)
|
||||||
|
|
||||||
|
### **✅ Visualização Melhorada:**
|
||||||
|
- Modal maior para acomodar mais informações
|
||||||
|
- Scroll automático quando há muitos itens
|
||||||
|
- Seções organizadas e claras
|
||||||
|
- Responsivo para mobile
|
||||||
|
|
||||||
|
### **✅ Informações Detalhadas:**
|
||||||
|
- Todos os itens da venda (originais + trocados)
|
||||||
|
- Histórico cronológico de operações
|
||||||
|
- Valores atualizados após trocas
|
||||||
|
- Status visual claro
|
||||||
|
|
||||||
|
## 5. **Exemplo de Uso**
|
||||||
|
|
||||||
|
### **Cenário: Troca do Produto 6-Branco pelo 4-Jeans**
|
||||||
|
|
||||||
|
**Antes da Correção:**
|
||||||
|
- ❌ Status: "Concluída" (não indicava troca)
|
||||||
|
- ❌ Não mostrava histórico de troca
|
||||||
|
- ❌ Produtos cortados no modal
|
||||||
|
- ❌ Sem informação sobre a operação
|
||||||
|
|
||||||
|
**Depois da Correção:**
|
||||||
|
- ✅ **Status:** "Com Troca" (badge amarelo)
|
||||||
|
- ✅ **Histórico:** Seção dedicada mostrando:
|
||||||
|
- Data: 10/10/2025
|
||||||
|
- Tipo: Troca
|
||||||
|
- Quantidade: 1
|
||||||
|
- Valor: R$ XX,XX
|
||||||
|
- ✅ **Itens:** Mostra todos os produtos (original + trocado)
|
||||||
|
- ✅ **Modal:** Expandido com scroll automático
|
||||||
|
|
||||||
|
## 6. **Fluxo Completo Corrigido**
|
||||||
|
|
||||||
|
### **1. Realizar Troca:**
|
||||||
|
1. Cliente troca produto 6-Branco por 4-Jeans
|
||||||
|
2. Sistema registra devolução + novo item
|
||||||
|
3. Estoque atualizado corretamente
|
||||||
|
|
||||||
|
### **2. Visualizar Venda:**
|
||||||
|
1. Status mostra "Com Troca"
|
||||||
|
2. Histórico exibe a operação
|
||||||
|
3. Itens mostram todos os produtos
|
||||||
|
4. Modal expandido e organizado
|
||||||
|
|
||||||
|
### **3. Informações Disponíveis:**
|
||||||
|
- ✅ Data e hora da troca
|
||||||
|
- ✅ Produtos envolvidos
|
||||||
|
- ✅ Valores atualizados
|
||||||
|
- ✅ Status visual claro
|
||||||
|
|
||||||
|
## 🚀 **Benefícios Alcançados**
|
||||||
|
|
||||||
|
- **✅ Transparência Total:** Histórico completo de operações
|
||||||
|
- **✅ Interface Profissional:** Modal organizado e responsivo
|
||||||
|
- **✅ Status Claro:** Identificação visual de vendas com troca
|
||||||
|
- **✅ Informações Completas:** Todos os dados em um local
|
||||||
|
- **✅ Experiência Melhorada:** Navegação intuitiva
|
||||||
|
- **✅ Controle Preciso:** Rastreamento de todas as operações
|
||||||
|
|
||||||
|
## 📋 **Como Testar**
|
||||||
|
|
||||||
|
1. **Acesse uma venda que teve troca**
|
||||||
|
2. **Clique no ícone 👁️ (Visualizar)**
|
||||||
|
3. **Verifique:**
|
||||||
|
- Status "Com Troca" (badge amarelo)
|
||||||
|
- Seção "Histórico de Devoluções/Trocas"
|
||||||
|
- Todos os itens visíveis com scroll
|
||||||
|
- Informações detalhadas da operação
|
||||||
|
|
||||||
|
## 🎯 **Status Final**
|
||||||
|
|
||||||
|
**✅ PROBLEMA COMPLETAMENTE RESOLVIDO**
|
||||||
|
|
||||||
|
A interface agora mostra:
|
||||||
|
- ✅ Status correto das vendas com troca
|
||||||
|
- ✅ Histórico completo de operações
|
||||||
|
- ✅ Todos os produtos visíveis (sem corte)
|
||||||
|
- ✅ Informações detalhadas e organizadas
|
||||||
|
- ✅ Modal responsivo e profissional
|
||||||
|
|
||||||
|
**Teste agora a venda que teve a troca do produto 6-Branco pelo 4-Jeans e veja todas as informações organizadas e claras!** 🎉
|
||||||
BIN
LogoLiberiKids.png
Normal file
BIN
LogoLiberiKids.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
252
MERCADOPAGO-PRODUCAO-GUIDE.md
Normal file
252
MERCADOPAGO-PRODUCAO-GUIDE.md
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
# 🏦 Guia Completo: PIX Produção com Mercado Pago
|
||||||
|
|
||||||
|
## 📋 **REQUISITOS OBRIGATÓRIOS**
|
||||||
|
|
||||||
|
### **1. Conta Mercado Pago Empresarial**
|
||||||
|
- ❌ **NÃO use conta pessoal**
|
||||||
|
- ✅ **Conta empresarial com CNPJ**
|
||||||
|
- ✅ **Verificação de identidade completa**
|
||||||
|
- ✅ **Dados bancários confirmados**
|
||||||
|
|
||||||
|
### **2. Documentação Necessária**
|
||||||
|
- **CNPJ** da empresa
|
||||||
|
- **Comprovante de endereço** da empresa
|
||||||
|
- **Documento do representante legal**
|
||||||
|
- **Conta bancária** em nome da empresa
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 **PASSO A PASSO COMPLETO**
|
||||||
|
|
||||||
|
### **ETAPA 1: Criar Conta Empresarial**
|
||||||
|
|
||||||
|
1. **Acesse:** https://www.mercadopago.com.br/developers
|
||||||
|
2. **Clique em:** "Criar conta"
|
||||||
|
3. **Selecione:** "Conta empresarial"
|
||||||
|
4. **Preencha:**
|
||||||
|
- CNPJ da empresa
|
||||||
|
- Razão social
|
||||||
|
- Email empresarial
|
||||||
|
- Telefone da empresa
|
||||||
|
|
||||||
|
### **ETAPA 2: Verificação de Identidade**
|
||||||
|
|
||||||
|
1. **Upload de documentos:**
|
||||||
|
- Cartão CNPJ
|
||||||
|
- Comprovante de endereço (máx 3 meses)
|
||||||
|
- RG/CNH do representante legal
|
||||||
|
|
||||||
|
2. **Aguardar aprovação:**
|
||||||
|
- Processo: 1-3 dias úteis
|
||||||
|
- Email de confirmação
|
||||||
|
|
||||||
|
### **ETAPA 3: Configurar Conta Bancária**
|
||||||
|
|
||||||
|
1. **No painel Mercado Pago:**
|
||||||
|
- Vá em "Conta" → "Dados bancários"
|
||||||
|
- Adicione conta em nome da empresa
|
||||||
|
- Confirme com micro depósitos
|
||||||
|
|
||||||
|
2. **Ativar PIX:**
|
||||||
|
- Vá em "Conta" → "PIX"
|
||||||
|
- Ative PIX para recebimentos
|
||||||
|
- Configure chaves PIX
|
||||||
|
|
||||||
|
### **ETAPA 4: Criar Aplicação**
|
||||||
|
|
||||||
|
1. **No painel de desenvolvedores:**
|
||||||
|
- Clique em "Criar aplicação"
|
||||||
|
- **Nome:** Liberi Kids - Sistema de Vendas
|
||||||
|
- **Produto:** Checkout Pro + Pagamentos online
|
||||||
|
|
||||||
|
2. **Configurar URLs:**
|
||||||
|
```
|
||||||
|
URL de sucesso: https://seudominio.com/sucesso
|
||||||
|
URL de falha: https://seudominio.com/falha
|
||||||
|
URL de webhook: https://seudominio.com/api/pix/webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Obter credenciais:**
|
||||||
|
- Vá em "Credenciais"
|
||||||
|
- Copie **Access Token** e **Public Key**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 **TIPOS DE CREDENCIAIS**
|
||||||
|
|
||||||
|
### **Desenvolvimento (Sandbox):**
|
||||||
|
```env
|
||||||
|
MERCADOPAGO_ACCESS_TOKEN=TEST-1234567890-abcdef...
|
||||||
|
MERCADOPAGO_PUBLIC_KEY=pk_test_1234567890abcdef...
|
||||||
|
BASE_URL=http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Produção (Vendas Reais):**
|
||||||
|
```env
|
||||||
|
MERCADOPAGO_ACCESS_TOKEN=APP-1234567890-abcdef...
|
||||||
|
MERCADOPAGO_PUBLIC_KEY=pk_live_1234567890abcdef...
|
||||||
|
BASE_URL=https://seudominio.com
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ **CONFIGURAÇÃO NO SISTEMA**
|
||||||
|
|
||||||
|
### **Opção 1: Script Automático (Recomendado)**
|
||||||
|
```bash
|
||||||
|
# Execute o script de configuração:
|
||||||
|
./configurar-producao-pix.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Opção 2: Manual**
|
||||||
|
```bash
|
||||||
|
# Editar .env:
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# Configurar:
|
||||||
|
MERCADOPAGO_ACCESS_TOKEN=APP-sua_access_token_real
|
||||||
|
MERCADOPAGO_PUBLIC_KEY=pk_live_sua_public_key_real
|
||||||
|
BASE_URL=https://seudominio.com
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# Ativar versão de produção:
|
||||||
|
sed -i "s|mercadopago-demo|mercadopago|g" server-supabase.js
|
||||||
|
|
||||||
|
# Reiniciar servidor:
|
||||||
|
pm2 restart liberi-kids
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 **CONFIGURAR WEBHOOK**
|
||||||
|
|
||||||
|
### **1. No Painel Mercado Pago:**
|
||||||
|
- Acesse sua aplicação
|
||||||
|
- Vá em "Webhooks"
|
||||||
|
- Clique em "Configurar webhook"
|
||||||
|
|
||||||
|
### **2. Configurações:**
|
||||||
|
```
|
||||||
|
URL: https://seudominio.com/api/pix/webhook
|
||||||
|
Eventos: payment
|
||||||
|
Versão: v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. Testar Webhook:**
|
||||||
|
- Use a ferramenta de teste do Mercado Pago
|
||||||
|
- Verifique logs do servidor
|
||||||
|
- Confirme recebimento das notificações
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 **TESTES OBRIGATÓRIOS**
|
||||||
|
|
||||||
|
### **1. Teste de Valor Baixo:**
|
||||||
|
```bash
|
||||||
|
# Gerar PIX de R$ 0,01:
|
||||||
|
curl -X POST https://seudominio.com/api/pix/gerar \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"venda_id": "teste001", "valor": 0.01}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Verificações:**
|
||||||
|
- ✅ QR Code gerado corretamente
|
||||||
|
- ✅ Pagamento processado
|
||||||
|
- ✅ Webhook recebido
|
||||||
|
- ✅ Valor creditado na conta
|
||||||
|
- ✅ Status atualizado no sistema
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 **CUSTOS E TARIFAS**
|
||||||
|
|
||||||
|
### **PIX:**
|
||||||
|
- **Taxa:** 0,99% por transação
|
||||||
|
- **Sem mensalidade**
|
||||||
|
- **Recebimento:** D+1 (1 dia útil)
|
||||||
|
- **Limite:** Até R$ 1.000 por transação (padrão)
|
||||||
|
|
||||||
|
### **Outros Métodos:**
|
||||||
|
- **Cartão de Crédito:** 2,99% + R$ 0,39
|
||||||
|
- **Cartão de Débito:** 1,99% + R$ 0,39
|
||||||
|
- **Boleto:** R$ 3,49 por boleto
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 **SEGURANÇA E BOAS PRÁTICAS**
|
||||||
|
|
||||||
|
### **1. Proteção de Credenciais:**
|
||||||
|
```bash
|
||||||
|
# Nunca commite .env no Git:
|
||||||
|
echo ".env" >> .gitignore
|
||||||
|
|
||||||
|
# Use variáveis de ambiente no servidor:
|
||||||
|
export MERCADOPAGO_ACCESS_TOKEN="APP-..."
|
||||||
|
export MERCADOPAGO_PUBLIC_KEY="pk_live_..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. HTTPS Obrigatório:**
|
||||||
|
- Use certificado SSL válido
|
||||||
|
- Configure redirect HTTP → HTTPS
|
||||||
|
- Webhook só funciona com HTTPS
|
||||||
|
|
||||||
|
### **3. Monitoramento:**
|
||||||
|
- Configure logs de transações
|
||||||
|
- Monitore webhooks perdidos
|
||||||
|
- Alerte para falhas de pagamento
|
||||||
|
|
||||||
|
### **4. Backup:**
|
||||||
|
- Backup regular do banco de dados
|
||||||
|
- Logs de todas as transações
|
||||||
|
- Histórico de webhooks recebidos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 **PROBLEMAS COMUNS**
|
||||||
|
|
||||||
|
### **Erro: "Unauthorized"**
|
||||||
|
- ✅ Verificar se credenciais são de produção (APP-...)
|
||||||
|
- ✅ Confirmar se conta está verificada
|
||||||
|
- ✅ Verificar se PIX está ativado
|
||||||
|
|
||||||
|
### **Erro: "Invalid webhook URL"**
|
||||||
|
- ✅ URL deve usar HTTPS
|
||||||
|
- ✅ Servidor deve estar acessível publicamente
|
||||||
|
- ✅ Endpoint /api/pix/webhook deve existir
|
||||||
|
|
||||||
|
### **PIX não aparece:**
|
||||||
|
- ✅ Aplicar SQL no Supabase
|
||||||
|
- ✅ Reiniciar servidor
|
||||||
|
- ✅ Limpar cache do navegador
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 **SUPORTE**
|
||||||
|
|
||||||
|
### **Mercado Pago:**
|
||||||
|
- **Documentação:** https://www.mercadopago.com.br/developers/pt/docs
|
||||||
|
- **Suporte:** https://www.mercadopago.com.br/ajuda
|
||||||
|
- **Status:** https://status.mercadopago.com/
|
||||||
|
|
||||||
|
### **Liberi Kids:**
|
||||||
|
- Verifique logs do servidor
|
||||||
|
- Teste APIs individualmente
|
||||||
|
- Confirme configurações do .env
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ **CHECKLIST FINAL**
|
||||||
|
|
||||||
|
- [ ] Conta Mercado Pago empresarial criada
|
||||||
|
- [ ] Verificação de identidade aprovada
|
||||||
|
- [ ] Conta bancária configurada
|
||||||
|
- [ ] PIX ativado na conta
|
||||||
|
- [ ] Aplicação criada no painel
|
||||||
|
- [ ] Credenciais de produção obtidas
|
||||||
|
- [ ] Sistema configurado com credenciais reais
|
||||||
|
- [ ] Webhook configurado e testado
|
||||||
|
- [ ] Teste de pagamento realizado
|
||||||
|
- [ ] HTTPS configurado
|
||||||
|
- [ ] Monitoramento ativo
|
||||||
|
|
||||||
|
**Após completar todos os itens, seu PIX de produção estará funcionando!** 🎉
|
||||||
161
README-DEPLOY.md
Normal file
161
README-DEPLOY.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# 🚀 Deploy Rápido - Liberi Kids
|
||||||
|
|
||||||
|
## ⚡ Deploy em 3 Comandos
|
||||||
|
|
||||||
|
### 1. 🖥️ **Servidor Local (Recomendado para começar)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Configure suas credenciais
|
||||||
|
cp .env.example .env
|
||||||
|
# Edite o .env com suas credenciais do Supabase
|
||||||
|
|
||||||
|
# 2. Execute o deploy automático
|
||||||
|
npm run deploy:local
|
||||||
|
|
||||||
|
# 3. Acesse: http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. ☁️ **Nuvem Gratuita (Vercel)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Deploy automático
|
||||||
|
npm run deploy:vercel
|
||||||
|
|
||||||
|
# 2. Configure variáveis no dashboard do Vercel
|
||||||
|
# 3. Acesse a URL fornecida
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 🐳 **Docker (Containerizado)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Configure o .env
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# 2. Execute com Docker
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 3. Acesse: http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 **Pré-requisitos Mínimos**
|
||||||
|
|
||||||
|
- ✅ Node.js 18+
|
||||||
|
- ✅ Conta no Supabase (gratuita)
|
||||||
|
- ✅ Credenciais do Supabase configuradas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 **Configuração Rápida do Supabase**
|
||||||
|
|
||||||
|
1. **Acesse:** https://supabase.com/dashboard
|
||||||
|
2. **Crie um projeto** (ou use existente)
|
||||||
|
3. **Vá em Settings > API**
|
||||||
|
4. **Copie:**
|
||||||
|
- Project URL → `SUPABASE_URL`
|
||||||
|
- anon public key → `SUPABASE_ANON_KEY`
|
||||||
|
5. **Cole no arquivo .env**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 **Opções de Hospedagem**
|
||||||
|
|
||||||
|
| Plataforma | Custo | Facilidade | Recomendado Para |
|
||||||
|
|------------|-------|------------|------------------|
|
||||||
|
| **Vercel** | Gratuito | ⭐⭐⭐⭐⭐ | Pequenas empresas |
|
||||||
|
| **Railway** | $5/mês | ⭐⭐⭐⭐ | Empresas médias |
|
||||||
|
| **VPS** | $5-20/mês | ⭐⭐⭐ | Controle total |
|
||||||
|
| **Local** | Gratuito | ⭐⭐⭐⭐ | Desenvolvimento |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 **Resolução de Problemas**
|
||||||
|
|
||||||
|
### ❌ Erro: "SUPABASE_URL não definida"
|
||||||
|
```bash
|
||||||
|
# Solução: Configure o arquivo .env
|
||||||
|
cp .env.example .env
|
||||||
|
# Edite com suas credenciais
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Erro: "Porta 5000 em uso"
|
||||||
|
```bash
|
||||||
|
# Solução: Mate processos na porta
|
||||||
|
sudo lsof -ti:5000 | xargs kill -9
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Erro: "Build falhou"
|
||||||
|
```bash
|
||||||
|
# Solução: Limpe e reinstale
|
||||||
|
rm -rf node_modules client/node_modules
|
||||||
|
npm install
|
||||||
|
cd client && npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 **Acesso Móvel**
|
||||||
|
|
||||||
|
A aplicação é **PWA** (Progressive Web App):
|
||||||
|
- ✅ Funciona no celular
|
||||||
|
- ✅ Pode ser "instalada"
|
||||||
|
- ✅ Interface responsiva
|
||||||
|
- ✅ Funciona offline (parcialmente)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 **Segurança**
|
||||||
|
|
||||||
|
### Para Produção:
|
||||||
|
- ✅ Use HTTPS sempre
|
||||||
|
- ✅ Configure firewall
|
||||||
|
- ✅ Mantenha Node.js atualizado
|
||||||
|
- ✅ Use variáveis de ambiente
|
||||||
|
- ✅ Backup regular do Supabase
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 **Suporte Rápido**
|
||||||
|
|
||||||
|
### Logs de Erro:
|
||||||
|
```bash
|
||||||
|
# PM2 (local)
|
||||||
|
pm2 logs liberi-kids
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker logs liberi-kids-app
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
vercel logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comandos Úteis:
|
||||||
|
```bash
|
||||||
|
# Reiniciar aplicação
|
||||||
|
pm2 restart liberi-kids
|
||||||
|
|
||||||
|
# Status do servidor
|
||||||
|
pm2 status
|
||||||
|
|
||||||
|
# Parar aplicação
|
||||||
|
pm2 stop liberi-kids
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **Próximos Passos**
|
||||||
|
|
||||||
|
Após o deploy:
|
||||||
|
|
||||||
|
1. ✅ **Teste todas as funcionalidades**
|
||||||
|
2. ✅ **Configure backup automático**
|
||||||
|
3. ✅ **Monitore performance**
|
||||||
|
4. ✅ **Configure domínio próprio** (opcional)
|
||||||
|
5. ✅ **Treine usuários**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🎉 Sua aplicação está pronta para o mundo real!**
|
||||||
|
|
||||||
|
Para suporte detalhado, consulte: `DEPLOY-GUIDE.md`
|
||||||
210
README.md
Normal file
210
README.md
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
# Liberi Kids - Sistema de Controle de Estoque
|
||||||
|
|
||||||
|
Sistema completo de controle de estoque desenvolvido especificamente para a **Liberi Kids - Moda Infantil**. Uma solução moderna e intuitiva para gerenciar produtos, clientes, fornecedores, despesas e vendas.
|
||||||
|
|
||||||
|
## 🚀 Funcionalidades
|
||||||
|
|
||||||
|
### 📦 Gestão de Produtos
|
||||||
|
- Cadastro completo de produtos com marca, nome, estação e valores
|
||||||
|
- Sistema de variações (tamanho, cor, quantidade)
|
||||||
|
- Upload de fotos para cada variação
|
||||||
|
- Controle de estoque em tempo real
|
||||||
|
- Vinculação com fornecedores
|
||||||
|
|
||||||
|
### 👥 Gestão de Clientes
|
||||||
|
- Cadastro completo com dados de contato
|
||||||
|
- Histórico de compras
|
||||||
|
- Informações de endereço e WhatsApp
|
||||||
|
|
||||||
|
### 🚛 Gestão de Fornecedores
|
||||||
|
- Cadastro de fornecedores com dados comerciais
|
||||||
|
- Controle de contatos (telefone, WhatsApp, e-mail)
|
||||||
|
- Vinculação com produtos
|
||||||
|
|
||||||
|
### 💰 Controle de Despesas
|
||||||
|
- Cadastro de diferentes tipos de despesas
|
||||||
|
- Vinculação com fornecedores
|
||||||
|
- Controle por data e valor
|
||||||
|
- Relatórios de gastos mensais
|
||||||
|
|
||||||
|
### 🛒 Sistema de Vendas
|
||||||
|
- Vendas à vista e parceladas
|
||||||
|
- Controle de itens vendidos
|
||||||
|
- Cálculo automático de totais
|
||||||
|
- Aplicação de descontos
|
||||||
|
- Histórico completo de vendas
|
||||||
|
|
||||||
|
### 📊 Dashboard Intuitivo
|
||||||
|
- Métricas em tempo real
|
||||||
|
- Gráficos de vendas por mês
|
||||||
|
- Distribuição de produtos por estação
|
||||||
|
- Resumo financeiro
|
||||||
|
- Indicadores de performance
|
||||||
|
|
||||||
|
## 🛠️ Tecnologias Utilizadas
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Node.js** - Runtime JavaScript
|
||||||
|
- **Express.js** - Framework web
|
||||||
|
- **SQLite** - Banco de dados
|
||||||
|
- **Multer** - Upload de arquivos
|
||||||
|
- **UUID** - Geração de IDs únicos
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **React** - Biblioteca de interface
|
||||||
|
- **React Router** - Roteamento
|
||||||
|
- **Axios** - Cliente HTTP
|
||||||
|
- **React Icons** - Ícones
|
||||||
|
- **Recharts** - Gráficos
|
||||||
|
- **React Hook Form** - Formulários
|
||||||
|
- **React Hot Toast** - Notificações
|
||||||
|
|
||||||
|
## 📋 Pré-requisitos
|
||||||
|
|
||||||
|
- Node.js (versão 14 ou superior)
|
||||||
|
- NPM ou Yarn
|
||||||
|
|
||||||
|
## 🔧 Instalação
|
||||||
|
|
||||||
|
1. **Clone o repositório ou navegue até a pasta do projeto:**
|
||||||
|
```bash
|
||||||
|
cd /home/tiago/Downloads/app_estoque
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Instale as dependências do backend:**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Instale as dependências do frontend:**
|
||||||
|
```bash
|
||||||
|
cd client
|
||||||
|
npm install
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Como Executar
|
||||||
|
|
||||||
|
### Desenvolvimento
|
||||||
|
|
||||||
|
1. **Inicie o servidor backend:**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
O servidor será executado na porta 5000.
|
||||||
|
|
||||||
|
2. **Em outro terminal, inicie o frontend:**
|
||||||
|
```bash
|
||||||
|
npm run client
|
||||||
|
```
|
||||||
|
O frontend será executado na porta 3000.
|
||||||
|
|
||||||
|
3. **Acesse o sistema:**
|
||||||
|
Abra seu navegador e vá para `http://localhost:3000`
|
||||||
|
|
||||||
|
### Produção
|
||||||
|
|
||||||
|
1. **Build do frontend:**
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Inicie o servidor:**
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Estrutura do Projeto
|
||||||
|
|
||||||
|
```
|
||||||
|
app_estoque/
|
||||||
|
├── client/ # Frontend React
|
||||||
|
│ ├── public/
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # Componentes reutilizáveis
|
||||||
|
│ │ ├── pages/ # Páginas da aplicação
|
||||||
|
│ │ ├── services/ # Serviços de API
|
||||||
|
│ │ └── utils/ # Utilitários
|
||||||
|
├── uploads/ # Arquivos enviados
|
||||||
|
├── server.js # Servidor principal
|
||||||
|
├── liberi_kids.db # Banco de dados SQLite
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Como Usar
|
||||||
|
|
||||||
|
### 1. Dashboard
|
||||||
|
- Visualize métricas gerais do negócio
|
||||||
|
- Acompanhe vendas, estoque e despesas
|
||||||
|
- Acesse ações rápidas
|
||||||
|
|
||||||
|
### 2. Produtos
|
||||||
|
- Cadastre novos produtos com todas as informações
|
||||||
|
- Adicione variações (tamanho, cor, quantidade)
|
||||||
|
- Faça upload de fotos para cada variação
|
||||||
|
- Gerencie o estoque
|
||||||
|
|
||||||
|
### 3. Clientes
|
||||||
|
- Cadastre clientes com dados completos
|
||||||
|
- Mantenha histórico de contatos
|
||||||
|
- Organize informações para vendas
|
||||||
|
|
||||||
|
### 4. Fornecedores
|
||||||
|
- Registre fornecedores e seus dados
|
||||||
|
- Mantenha contatos organizados
|
||||||
|
- Vincule produtos aos fornecedores
|
||||||
|
|
||||||
|
### 5. Despesas
|
||||||
|
- Crie tipos de despesas personalizados
|
||||||
|
- Registre todos os gastos da empresa
|
||||||
|
- Acompanhe despesas por período
|
||||||
|
|
||||||
|
### 6. Vendas
|
||||||
|
- Registre vendas à vista ou parceladas
|
||||||
|
- Adicione múltiplos itens por venda
|
||||||
|
- Aplique descontos
|
||||||
|
- Controle o estoque automaticamente
|
||||||
|
|
||||||
|
## 🔒 Segurança
|
||||||
|
|
||||||
|
- Upload de arquivos com validação de tipo
|
||||||
|
- Sanitização de dados de entrada
|
||||||
|
- Controle de acesso às rotas da API
|
||||||
|
|
||||||
|
## 📱 Responsividade
|
||||||
|
|
||||||
|
O sistema é totalmente responsivo e funciona perfeitamente em:
|
||||||
|
- Desktop
|
||||||
|
- Tablets
|
||||||
|
- Smartphones
|
||||||
|
|
||||||
|
## 🎨 Design
|
||||||
|
|
||||||
|
- Interface moderna e intuitiva
|
||||||
|
- Cores e tipografia profissionais
|
||||||
|
- Experiência de usuário otimizada
|
||||||
|
- Animações suaves e feedback visual
|
||||||
|
|
||||||
|
## 📈 Próximas Funcionalidades
|
||||||
|
|
||||||
|
- [ ] Sistema de backup automático
|
||||||
|
- [ ] Relatórios em PDF
|
||||||
|
- [ ] Integração com WhatsApp
|
||||||
|
- [ ] App mobile nativo
|
||||||
|
- [ ] Sistema de usuários e permissões
|
||||||
|
|
||||||
|
## 🐛 Problemas Conhecidos
|
||||||
|
|
||||||
|
Nenhum problema conhecido no momento.
|
||||||
|
|
||||||
|
## 📞 Suporte
|
||||||
|
|
||||||
|
Para suporte técnico ou dúvidas sobre o sistema, entre em contato através dos canais oficiais da Liberi Kids.
|
||||||
|
|
||||||
|
## 📄 Licença
|
||||||
|
|
||||||
|
Este projeto foi desenvolvido exclusivamente para a **Liberi Kids - Moda Infantil**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Desenvolvido com ❤️ para a Liberi Kids - Moda Infantil**
|
||||||
231
SISTEMA-DEVOLUCOES-CORRIGIDO.md
Normal file
231
SISTEMA-DEVOLUCOES-CORRIGIDO.md
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# 🔧 Sistema de Devoluções/Trocas - CORREÇÃO DEFINITIVA
|
||||||
|
|
||||||
|
## ❌ **Problemas Críticos Identificados e Resolvidos**
|
||||||
|
|
||||||
|
### **Problema Original:**
|
||||||
|
- ✅ **RESOLVIDO:** Devoluções não zeravam quantidade do item na venda
|
||||||
|
- ✅ **RESOLVIDO:** Sistema permitia devolver o mesmo item múltiplas vezes
|
||||||
|
- ✅ **RESOLVIDO:** Valor da venda não era atualizado corretamente
|
||||||
|
- ✅ **RESOLVIDO:** Falta de controle de disponibilidade para devolução
|
||||||
|
- ✅ **RESOLVIDO:** Histórico incompleto das operações
|
||||||
|
|
||||||
|
## 🔧 **Soluções Implementadas**
|
||||||
|
|
||||||
|
### **1. Controle Correto de Quantidades**
|
||||||
|
|
||||||
|
**Antes:**
|
||||||
|
```javascript
|
||||||
|
// Item permanecia com quantidade original na venda
|
||||||
|
// Permitia devoluções infinitas do mesmo item
|
||||||
|
```
|
||||||
|
|
||||||
|
**Depois:**
|
||||||
|
```javascript
|
||||||
|
// ATUALIZAR ITEM DA VENDA - ZERAR QUANTIDADE DEVOLVIDA
|
||||||
|
const novaQuantidadeItem = parseInt(itemOriginal.quantidade) - parseInt(quantidade_devolvida);
|
||||||
|
const novoValorTotal = novaQuantidadeItem * parseFloat(itemOriginal.valor_unitario);
|
||||||
|
|
||||||
|
const { error: updateItemError } = await supabase
|
||||||
|
.from('venda_itens')
|
||||||
|
.update({
|
||||||
|
quantidade: novaQuantidadeItem, // ZERA se devolução completa
|
||||||
|
valor_total: novoValorTotal // ZERA valor proporcionalmente
|
||||||
|
})
|
||||||
|
.eq('id', item_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Validação Anti-Duplicação**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// VERIFICAR SE JÁ FOI DEVOLVIDO COMPLETAMENTE
|
||||||
|
const quantidadeDisponivel = parseInt(itemOriginal.quantidade) - quantidadeJaDevolvida;
|
||||||
|
|
||||||
|
if (quantidadeDisponivel <= 0) {
|
||||||
|
throw new Error(`Este item já foi completamente devolvido/trocado anteriormente`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. Controle de Disponibilidade**
|
||||||
|
|
||||||
|
**API GET /api/devolucoes/vendas - VERSÃO CORRIGIDA:**
|
||||||
|
```javascript
|
||||||
|
// Só incluir se ainda há quantidade disponível para devolução
|
||||||
|
if (quantidadeDisponivel > 0) {
|
||||||
|
itensDisponiveis.push({
|
||||||
|
...item,
|
||||||
|
quantidade_disponivel: quantidadeDisponivel,
|
||||||
|
quantidade_original: parseInt(item.quantidade),
|
||||||
|
quantidade_devolvida: quantidadeDevolvida,
|
||||||
|
pode_devolver: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrar apenas vendas que ainda têm itens disponíveis
|
||||||
|
const vendasComItens = vendasProcessadas.filter(venda => venda.tem_itens_disponiveis);
|
||||||
|
```
|
||||||
|
|
||||||
|
### **4. Histórico Completo**
|
||||||
|
|
||||||
|
**Nova API GET /api/devolucoes/venda/:venda_id:**
|
||||||
|
```javascript
|
||||||
|
const historico = data.map(dev => ({
|
||||||
|
...dev,
|
||||||
|
produto_info: {
|
||||||
|
nome: `${dev.venda_itens.produtos?.marca} - ${dev.venda_itens.produtos?.nome}`,
|
||||||
|
codigo: dev.venda_itens.produtos?.id_produto,
|
||||||
|
variacao: `${dev.venda_itens.produto_variacoes?.tamanho} - ${dev.venda_itens.produto_variacoes?.cor}`,
|
||||||
|
quantidade_original: dev.venda_itens.quantidade_original,
|
||||||
|
valor_unitario_original: dev.venda_itens.valor_unitario_original
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 **Fluxos Corrigidos**
|
||||||
|
|
||||||
|
### **Devolução Simples:**
|
||||||
|
1. ✅ **Verificar disponibilidade:** Item ainda pode ser devolvido?
|
||||||
|
2. ✅ **Atualizar venda:** Reduzir quantidade e valor do item
|
||||||
|
3. ✅ **Retornar ao estoque:** Produto volta ao estoque
|
||||||
|
4. ✅ **Registrar histórico:** Data, motivo, quantidades, valores
|
||||||
|
5. ✅ **Atualizar valor da venda:** Novo total calculado
|
||||||
|
|
||||||
|
### **Troca de Produto:**
|
||||||
|
1. ✅ **Validar estoque:** Produto novo tem estoque?
|
||||||
|
2. ✅ **Processar devolução:** Item original zerado na venda
|
||||||
|
3. ✅ **Retornar ao estoque:** Produto original volta
|
||||||
|
4. ✅ **Reduzir estoque:** Produto novo sai do estoque
|
||||||
|
5. ✅ **Adicionar à venda:** Novo item adicionado
|
||||||
|
6. ✅ **Calcular diferença:** Atualizar valor total
|
||||||
|
7. ✅ **Registrar histórico:** Operação completa documentada
|
||||||
|
|
||||||
|
## 📊 **Exemplo Prático**
|
||||||
|
|
||||||
|
### **Cenário: Devolução de 1 Camiseta de uma venda de 2**
|
||||||
|
|
||||||
|
**Antes da Correção:**
|
||||||
|
- Venda: 2 camisetas por R$ 100,00
|
||||||
|
- Após devolução: Ainda mostrava 2 camisetas na venda
|
||||||
|
- Permitia devolver novamente as mesmas 2 camisetas
|
||||||
|
|
||||||
|
**Depois da Correção:**
|
||||||
|
- Venda: 2 camisetas por R$ 100,00
|
||||||
|
- Após devolução de 1: **1 camiseta por R$ 50,00**
|
||||||
|
- Sistema só permite devolver a 1 camiseta restante
|
||||||
|
- Histórico mostra: "1 camiseta devolvida em DD/MM/AAAA - Motivo: Defeito"
|
||||||
|
|
||||||
|
## 🛡️ **Validações Implementadas**
|
||||||
|
|
||||||
|
### **1. Controle de Quantidade:**
|
||||||
|
```javascript
|
||||||
|
if (parseInt(quantidade_devolvida) > quantidadeDisponivel) {
|
||||||
|
throw new Error(`Quantidade de devolução (${quantidade_devolvida}) maior que disponível (${quantidadeDisponivel})`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Verificação de Estoque (Trocas):**
|
||||||
|
```javascript
|
||||||
|
if (!variacao || variacao.quantidade < parseInt(quantidade)) {
|
||||||
|
throw new Error(`Estoque insuficiente para troca. Disponível: ${estoqueDisponivel}, solicitado: ${quantidade}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. Prevenção de Duplicatas:**
|
||||||
|
```javascript
|
||||||
|
if (quantidadeDisponivel <= 0) {
|
||||||
|
throw new Error(`Este item já foi completamente devolvido/trocado anteriormente`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 **Benefícios Alcançados**
|
||||||
|
|
||||||
|
### **✅ Controle Preciso:**
|
||||||
|
- Quantidades corretas em tempo real
|
||||||
|
- Impossível devolver mais que vendido
|
||||||
|
- Valores sempre atualizados
|
||||||
|
|
||||||
|
### **✅ Histórico Completo:**
|
||||||
|
- Data e hora de cada operação
|
||||||
|
- Motivo informado pelo usuário
|
||||||
|
- Produtos e quantidades envolvidas
|
||||||
|
- Valores originais e devolvidos
|
||||||
|
|
||||||
|
### **✅ Interface Inteligente:**
|
||||||
|
- Só mostra itens disponíveis para devolução
|
||||||
|
- Quantidades disponíveis vs originais
|
||||||
|
- Status visual claro
|
||||||
|
|
||||||
|
### **✅ Estoque Preciso:**
|
||||||
|
- Entrada correta em devoluções
|
||||||
|
- Controle bidirecional em trocas
|
||||||
|
- Logs detalhados de movimentação
|
||||||
|
|
||||||
|
## 🧪 **Como Testar**
|
||||||
|
|
||||||
|
### **1. Teste de Devolução:**
|
||||||
|
```bash
|
||||||
|
# 1. Faça uma venda de 2 produtos iguais
|
||||||
|
# 2. Vá em Devoluções/Trocas
|
||||||
|
# 3. Devolva 1 produto
|
||||||
|
# 4. Verifique: venda agora mostra apenas 1 produto
|
||||||
|
# 5. Tente devolver novamente: só permite devolver o 1 restante
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Teste de Troca:**
|
||||||
|
```bash
|
||||||
|
# 1. Faça uma venda
|
||||||
|
# 2. Vá em Devoluções/Trocas → Troca
|
||||||
|
# 3. Selecione produto para devolver + produto novo
|
||||||
|
# 4. Verifique:
|
||||||
|
# - Produto original zerado na venda
|
||||||
|
# - Produto novo adicionado à venda
|
||||||
|
# - Estoque atualizado corretamente
|
||||||
|
# - Valor da venda recalculado
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. Teste de Histórico:**
|
||||||
|
```bash
|
||||||
|
# 1. Após qualquer devolução/troca
|
||||||
|
# 2. Vá em Vendas → Visualizar venda
|
||||||
|
# 3. Verifique seção "Histórico de Devoluções/Trocas"
|
||||||
|
# 4. Confirme: data, motivo, produtos, quantidades
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 **APIs Corrigidas**
|
||||||
|
|
||||||
|
### **GET /api/devolucoes/vendas**
|
||||||
|
- ✅ Filtra apenas itens disponíveis para devolução
|
||||||
|
- ✅ Mostra quantidade disponível vs original
|
||||||
|
- ✅ Remove vendas sem itens disponíveis
|
||||||
|
|
||||||
|
### **POST /api/devolucoes**
|
||||||
|
- ✅ Atualiza quantidade e valor do item na venda
|
||||||
|
- ✅ Controla estoque corretamente
|
||||||
|
- ✅ Registra histórico completo
|
||||||
|
- ✅ Calcula valores corretamente
|
||||||
|
|
||||||
|
### **GET /api/devolucoes/venda/:id**
|
||||||
|
- ✅ Histórico detalhado por venda
|
||||||
|
- ✅ Informações completas dos produtos
|
||||||
|
- ✅ Quantidades e valores originais vs devolvidos
|
||||||
|
|
||||||
|
## 🎯 **Status Final**
|
||||||
|
|
||||||
|
**✅ PROBLEMA COMPLETAMENTE RESOLVIDO**
|
||||||
|
|
||||||
|
O sistema agora:
|
||||||
|
- ✅ **Zera quantidades** de itens devolvidos
|
||||||
|
- ✅ **Impede devoluções duplicadas** do mesmo item
|
||||||
|
- ✅ **Controla estoque precisamente** em devoluções e trocas
|
||||||
|
- ✅ **Atualiza valores** da venda automaticamente
|
||||||
|
- ✅ **Mantém histórico completo** de todas as operações
|
||||||
|
- ✅ **Mostra apenas itens disponíveis** para devolução
|
||||||
|
- ✅ **Valida todas as operações** antes de executar
|
||||||
|
|
||||||
|
## 🚀 **Teste Agora!**
|
||||||
|
|
||||||
|
1. **Faça uma devolução** e veja o item ser zerado na venda
|
||||||
|
2. **Tente devolver novamente** - sistema impedirá
|
||||||
|
3. **Faça uma troca** e veja o controle bidirecional de estoque
|
||||||
|
4. **Verifique o histórico** completo na visualização da venda
|
||||||
|
|
||||||
|
**O sistema está funcionando perfeitamente!** 🎉
|
||||||
148
SUPABASE-SETUP.md
Normal file
148
SUPABASE-SETUP.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# 🚀 Configuração do Supabase - Liberi Kids Estoque
|
||||||
|
|
||||||
|
## 📋 Instruções para Configurar o Banco de Dados
|
||||||
|
|
||||||
|
### 1. **Acesse o Supabase Dashboard**
|
||||||
|
- Vá para: https://xyqmlesqdqybiyjofysb.supabase.co
|
||||||
|
- Faça login na sua conta
|
||||||
|
|
||||||
|
### 2. **Execute o Script SQL**
|
||||||
|
1. No dashboard do Supabase, vá para **SQL Editor**
|
||||||
|
2. Clique em **New Query**
|
||||||
|
3. Copie e cole todo o conteúdo do arquivo `sql/create-tables.sql`
|
||||||
|
4. Clique em **Run** para executar
|
||||||
|
|
||||||
|
### 3. **Criar Função de Atualização de Estoque**
|
||||||
|
1. Ainda no SQL Editor, crie uma nova query
|
||||||
|
2. Copie e cole o conteúdo do arquivo `sql/functions.sql`
|
||||||
|
3. Execute a query
|
||||||
|
|
||||||
|
### 4. **Verificar Tabelas Criadas**
|
||||||
|
1. Vá para **Table Editor** no menu lateral
|
||||||
|
2. Você deve ver as seguintes tabelas:
|
||||||
|
- ✅ `fornecedores`
|
||||||
|
- ✅ `produtos`
|
||||||
|
- ✅ `produto_variacoes`
|
||||||
|
- ✅ `clientes`
|
||||||
|
- ✅ `tipos_despesas`
|
||||||
|
- ✅ `despesas`
|
||||||
|
- ✅ `vendas`
|
||||||
|
- ✅ `venda_itens`
|
||||||
|
|
||||||
|
### 5. **Configurar Políticas RLS (Row Level Security)**
|
||||||
|
As políticas já estão incluídas no script SQL, mas você pode ajustá-las conforme necessário:
|
||||||
|
|
||||||
|
1. Vá para **Authentication** > **Policies**
|
||||||
|
2. Verifique se todas as tabelas têm políticas habilitadas
|
||||||
|
3. As políticas atuais permitem todas as operações para usuários autenticados
|
||||||
|
|
||||||
|
### 6. **Testar a Conexão**
|
||||||
|
1. Inicie o servidor: `npm start`
|
||||||
|
2. Acesse: http://localhost:5000
|
||||||
|
3. Clique no botão "Testar API" na página de produtos
|
||||||
|
4. Deve aparecer: "API Supabase funcionando corretamente!"
|
||||||
|
|
||||||
|
## 🔧 **Estrutura das Tabelas**
|
||||||
|
|
||||||
|
### **Fornecedores**
|
||||||
|
- `id` (UUID, PK)
|
||||||
|
- `razao_social` (TEXT, NOT NULL)
|
||||||
|
- `telefone`, `whatsapp`, `endereco`, `email` (TEXT)
|
||||||
|
- `created_at`, `updated_at` (TIMESTAMP)
|
||||||
|
|
||||||
|
### **Produtos**
|
||||||
|
- `id` (UUID, PK)
|
||||||
|
- `id_produto` (TEXT) - ID personalizado
|
||||||
|
- `marca`, `nome` (TEXT, NOT NULL)
|
||||||
|
- `estacao` (TEXT, NOT NULL)
|
||||||
|
- `genero` (TEXT, DEFAULT 'Unissex')
|
||||||
|
- `fornecedor_id` (UUID, FK)
|
||||||
|
- `valor_compra`, `valor_revenda` (DECIMAL)
|
||||||
|
- `foto_principal_url` (TEXT)
|
||||||
|
- `created_at`, `updated_at` (TIMESTAMP)
|
||||||
|
|
||||||
|
### **Produto Variações**
|
||||||
|
- `id` (UUID, PK)
|
||||||
|
- `produto_id` (UUID, FK, NOT NULL)
|
||||||
|
- `tamanho`, `cor` (TEXT, NOT NULL)
|
||||||
|
- `quantidade` (INTEGER, DEFAULT 0)
|
||||||
|
- `foto_url` (TEXT)
|
||||||
|
- `created_at`, `updated_at` (TIMESTAMP)
|
||||||
|
|
||||||
|
### **Clientes**
|
||||||
|
- `id` (UUID, PK)
|
||||||
|
- `nome_completo` (TEXT, NOT NULL)
|
||||||
|
- `email`, `telefone`, `whatsapp`, `endereco` (TEXT)
|
||||||
|
- `created_at`, `updated_at` (TIMESTAMP)
|
||||||
|
|
||||||
|
### **Tipos de Despesas**
|
||||||
|
- `id` (UUID, PK)
|
||||||
|
- `nome` (TEXT, NOT NULL, UNIQUE)
|
||||||
|
- `descricao` (TEXT)
|
||||||
|
- `created_at` (TIMESTAMP)
|
||||||
|
|
||||||
|
### **Despesas**
|
||||||
|
- `id` (UUID, PK)
|
||||||
|
- `tipo_despesa_id` (UUID, FK, NOT NULL)
|
||||||
|
- `fornecedor_id` (UUID, FK)
|
||||||
|
- `data_despesa` (DATE, NOT NULL)
|
||||||
|
- `valor` (DECIMAL, NOT NULL)
|
||||||
|
- `descricao` (TEXT)
|
||||||
|
- `created_at`, `updated_at` (TIMESTAMP)
|
||||||
|
|
||||||
|
### **Vendas**
|
||||||
|
- `id` (UUID, PK)
|
||||||
|
- `cliente_id` (UUID, FK)
|
||||||
|
- `tipo_pagamento` (TEXT, CHECK: 'vista' ou 'parcelado')
|
||||||
|
- `valor_total` (DECIMAL, NOT NULL)
|
||||||
|
- `desconto` (DECIMAL, DEFAULT 0)
|
||||||
|
- `parcelas` (INTEGER, DEFAULT 1)
|
||||||
|
- `valor_parcela` (DECIMAL, DEFAULT 0)
|
||||||
|
- `data_venda` (DATE, NOT NULL)
|
||||||
|
- `observacoes` (TEXT)
|
||||||
|
- `created_at`, `updated_at` (TIMESTAMP)
|
||||||
|
|
||||||
|
### **Venda Itens**
|
||||||
|
- `id` (UUID, PK)
|
||||||
|
- `venda_id` (UUID, FK, NOT NULL)
|
||||||
|
- `produto_id` (UUID, FK, NOT NULL)
|
||||||
|
- `variacao_id` (UUID, FK)
|
||||||
|
- `quantidade` (INTEGER, NOT NULL)
|
||||||
|
- `valor_unitario`, `valor_total` (DECIMAL, NOT NULL)
|
||||||
|
- `created_at` (TIMESTAMP)
|
||||||
|
|
||||||
|
## 🎯 **Vantagens do Supabase**
|
||||||
|
|
||||||
|
✅ **Banco PostgreSQL robusto**
|
||||||
|
✅ **Backup automático**
|
||||||
|
✅ **Escalabilidade**
|
||||||
|
✅ **Interface web para gerenciamento**
|
||||||
|
✅ **APIs REST automáticas**
|
||||||
|
✅ **Segurança com RLS**
|
||||||
|
✅ **Sem problemas de concorrência**
|
||||||
|
|
||||||
|
## 🚨 **Comandos Importantes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Iniciar com Supabase (padrão)
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Iniciar com SQLite (backup)
|
||||||
|
npm run start-sqlite
|
||||||
|
|
||||||
|
# Desenvolvimento com Supabase
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Testar conexão com Supabase
|
||||||
|
npm run init-supabase
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 **Suporte**
|
||||||
|
|
||||||
|
Se houver algum problema:
|
||||||
|
1. Verifique se as tabelas foram criadas corretamente
|
||||||
|
2. Confirme se as políticas RLS estão ativas
|
||||||
|
3. Teste a conexão com o botão "Testar API"
|
||||||
|
4. Verifique os logs do servidor no terminal
|
||||||
|
|
||||||
|
**O sistema agora está usando Supabase e deve funcionar perfeitamente! 🎉**
|
||||||
256
TESTAR-PIX-COMPLETO.md
Normal file
256
TESTAR-PIX-COMPLETO.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# 🧪 Como Testar PIX - Guia Completo
|
||||||
|
|
||||||
|
## 📋 **Pré-requisitos para Teste**
|
||||||
|
|
||||||
|
Antes de testar, certifique-se que:
|
||||||
|
- ✅ Deploy foi feito para o servidor
|
||||||
|
- ✅ SQL foi aplicado no Supabase
|
||||||
|
- ✅ Credenciais Mercado Pago configuradas no .env
|
||||||
|
- ✅ Servidor reiniciado com PM2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 **1. Verificar Configuração no Servidor**
|
||||||
|
|
||||||
|
### **Conectar no servidor:**
|
||||||
|
```bash
|
||||||
|
ssh tiago@192.168.195.145
|
||||||
|
cd ~/app_estoque
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Verificar se .env está configurado:**
|
||||||
|
```bash
|
||||||
|
cat .env | grep MERCADOPAGO
|
||||||
|
```
|
||||||
|
|
||||||
|
**Deve mostrar algo como:**
|
||||||
|
```
|
||||||
|
MERCADOPAGO_ACCESS_TOKEN=TEST-1234567890-abcdef...
|
||||||
|
MERCADOPAGO_PUBLIC_KEY=pk_test_1234567890abcdef...
|
||||||
|
BASE_URL=http://192.168.195.145:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Verificar se servidor está rodando:**
|
||||||
|
```bash
|
||||||
|
pm2 status
|
||||||
|
pm2 logs liberi-kids --lines 20
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ **2. Verificar Banco de Dados**
|
||||||
|
|
||||||
|
### **No painel do Supabase:**
|
||||||
|
1. Acesse: https://supabase.com/dashboard
|
||||||
|
2. Vá em **SQL Editor**
|
||||||
|
3. Execute para verificar se colunas foram criadas:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT column_name, data_type, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'vendas'
|
||||||
|
AND column_name IN ('status_pagamento', 'data_pagamento', 'pix_payment_id', 'pix_qr_code', 'metodo_pagamento')
|
||||||
|
ORDER BY column_name;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Deve retornar 5 linhas com as colunas PIX.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 **3. Teste da Interface Web**
|
||||||
|
|
||||||
|
### **Acesse o sistema:**
|
||||||
|
```
|
||||||
|
http://192.168.195.145:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Navegue para Vendas:**
|
||||||
|
1. **Menu lateral** → **Vendas**
|
||||||
|
2. **Procure uma venda** na lista
|
||||||
|
3. **Verifique se aparece o botão PIX** (ícone 💳)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💳 **4. Teste do Modal PIX**
|
||||||
|
|
||||||
|
### **Clique no botão PIX de uma venda:**
|
||||||
|
|
||||||
|
**✅ O que deve acontecer:**
|
||||||
|
- Modal PIX abre instantaneamente
|
||||||
|
- Título: "Pagamento PIX - Venda #123"
|
||||||
|
- **QR Code** aparece (imagem quadrada)
|
||||||
|
- **Código PIX** aparece (texto longo para copiar)
|
||||||
|
- **Timer** mostra 30:00 (30 minutos)
|
||||||
|
- **Status:** "Aguardando pagamento..."
|
||||||
|
|
||||||
|
### **❌ Se der erro, verifique:**
|
||||||
|
- Console do navegador (F12)
|
||||||
|
- Logs do servidor: `pm2 logs liberi-kids`
|
||||||
|
- Se credenciais estão corretas no .env
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 **5. Teste das APIs Diretamente**
|
||||||
|
|
||||||
|
### **No servidor, teste as APIs:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Teste 1: Gerar PIX para venda ID 1
|
||||||
|
curl -X POST http://localhost:5000/api/pix/gerar \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"vendaId": 1}'
|
||||||
|
|
||||||
|
# Deve retornar: payment_id, qr_code, qr_code_base64
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Teste 2: Consultar status (use o payment_id do teste anterior)
|
||||||
|
curl http://localhost:5000/api/pix/status/SEU_PAYMENT_ID
|
||||||
|
|
||||||
|
# Deve retornar: status, payment_id, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 **6. Teste de Pagamento Real (Sandbox)**
|
||||||
|
|
||||||
|
### **Com credenciais TEST do Mercado Pago:**
|
||||||
|
|
||||||
|
1. **Gere um PIX** no sistema
|
||||||
|
2. **Use o app de teste** do Mercado Pago
|
||||||
|
3. **Escaneie o QR Code** ou **copie o código PIX**
|
||||||
|
4. **Simule o pagamento** no ambiente de teste
|
||||||
|
|
||||||
|
### **Cartões de teste Mercado Pago:**
|
||||||
|
```
|
||||||
|
Cartão aprovado: 4111 1111 1111 1111
|
||||||
|
CVV: 123
|
||||||
|
Vencimento: 12/25
|
||||||
|
Nome: APRO
|
||||||
|
CPF: 12345678909
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 **7. Teste do Webhook (Confirmação Automática)**
|
||||||
|
|
||||||
|
### **Configurar webhook no Mercado Pago:**
|
||||||
|
1. Acesse: https://www.mercadopago.com.br/developers
|
||||||
|
2. **Sua aplicação** → **Webhooks**
|
||||||
|
3. **URL:** `http://192.168.195.145:5000/api/pix/webhook`
|
||||||
|
4. **Eventos:** `payment`
|
||||||
|
|
||||||
|
### **Teste a confirmação:**
|
||||||
|
1. **Faça um pagamento** de teste
|
||||||
|
2. **Aguarde alguns segundos**
|
||||||
|
3. **Verifique se status mudou** para "Pago" no sistema
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 **8. Verificar Logs e Monitoramento**
|
||||||
|
|
||||||
|
### **Logs do servidor:**
|
||||||
|
```bash
|
||||||
|
# Ver logs em tempo real:
|
||||||
|
pm2 logs liberi-kids --lines 50
|
||||||
|
|
||||||
|
# Ver apenas erros:
|
||||||
|
pm2 logs liberi-kids --err
|
||||||
|
|
||||||
|
# Ver logs específicos do PIX:
|
||||||
|
pm2 logs liberi-kids | grep -i pix
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Console do navegador:**
|
||||||
|
- **F12** → **Console**
|
||||||
|
- Procure por erros em vermelho
|
||||||
|
- Verifique chamadas de API na aba **Network**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ **Checklist de Teste Completo**
|
||||||
|
|
||||||
|
### **Configuração:**
|
||||||
|
- [ ] Credenciais Mercado Pago no .env
|
||||||
|
- [ ] SQL aplicado no Supabase
|
||||||
|
- [ ] Servidor rodando (pm2 status)
|
||||||
|
- [ ] Build do frontend atualizado
|
||||||
|
|
||||||
|
### **Interface:**
|
||||||
|
- [ ] Botão PIX aparece nas vendas
|
||||||
|
- [ ] Modal PIX abre sem erro
|
||||||
|
- [ ] QR Code é exibido
|
||||||
|
- [ ] Código PIX aparece para copiar
|
||||||
|
- [ ] Timer funciona (30 minutos)
|
||||||
|
|
||||||
|
### **APIs:**
|
||||||
|
- [ ] POST /api/pix/gerar retorna payment_id
|
||||||
|
- [ ] GET /api/pix/status retorna dados corretos
|
||||||
|
- [ ] Webhook recebe confirmações
|
||||||
|
|
||||||
|
### **Pagamento:**
|
||||||
|
- [ ] QR Code pode ser escaneado
|
||||||
|
- [ ] Código PIX pode ser colado no banco
|
||||||
|
- [ ] Status atualiza automaticamente
|
||||||
|
- [ ] Venda fica marcada como "Paga"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 **Problemas Comuns e Soluções**
|
||||||
|
|
||||||
|
### **Erro: "Access token inválido"**
|
||||||
|
```bash
|
||||||
|
# Verifique se token está correto:
|
||||||
|
cat .env | grep MERCADOPAGO_ACCESS_TOKEN
|
||||||
|
# Deve começar com TEST- ou APP-
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Erro: "QR Code não aparece"**
|
||||||
|
```bash
|
||||||
|
# Verifique logs:
|
||||||
|
pm2 logs liberi-kids | grep -i error
|
||||||
|
# Reinstale dependência:
|
||||||
|
npm install mercadopago
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Erro: "Webhook não funciona"**
|
||||||
|
- Verifique se URL está correta no Mercado Pago
|
||||||
|
- Teste se servidor está acessível externamente
|
||||||
|
- Configure firewall se necessário
|
||||||
|
|
||||||
|
### **Erro: "Modal não abre"**
|
||||||
|
- Verifique console do navegador (F12)
|
||||||
|
- Limpe cache do navegador (Ctrl+F5)
|
||||||
|
- Verifique se build foi feito corretamente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **Teste Rápido (2 minutos)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Conectar no servidor
|
||||||
|
ssh tiago@192.168.195.145
|
||||||
|
|
||||||
|
# 2. Verificar status
|
||||||
|
cd ~/app_estoque && pm2 status
|
||||||
|
|
||||||
|
# 3. Testar API
|
||||||
|
curl -X POST http://localhost:5000/api/pix/gerar -H "Content-Type: application/json" -d '{"vendaId": 1}'
|
||||||
|
|
||||||
|
# 4. Acessar sistema
|
||||||
|
# http://192.168.195.145:5000 → Vendas → Botão PIX
|
||||||
|
```
|
||||||
|
|
||||||
|
**Se tudo funcionar, seu PIX está 100% operacional!** 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 **Suporte**
|
||||||
|
|
||||||
|
Se encontrar problemas:
|
||||||
|
1. **Verifique os logs** primeiro
|
||||||
|
2. **Teste as APIs** diretamente
|
||||||
|
3. **Confirme as credenciais** do Mercado Pago
|
||||||
|
4. **Reinicie o servidor** se necessário
|
||||||
|
|
||||||
|
**Seu sistema PIX está pronto para processar pagamentos reais!** 💳🏦
|
||||||
27
aplicar-pix-supabase.sql
Normal file
27
aplicar-pix-supabase.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
-- Script para aplicar campos PIX diretamente no Supabase
|
||||||
|
-- Execute este SQL no painel do Supabase ou via psql
|
||||||
|
|
||||||
|
-- Adicionar campos PIX na tabela vendas
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS status_pagamento VARCHAR(20) DEFAULT 'pendente';
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS data_pagamento TIMESTAMP;
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS pix_payment_id VARCHAR(100);
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS pix_qr_code TEXT;
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS metodo_pagamento VARCHAR(20) DEFAULT 'dinheiro';
|
||||||
|
|
||||||
|
-- Criar índices para performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vendas_status_pagamento ON vendas(status_pagamento);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vendas_pix_payment_id ON vendas(pix_payment_id);
|
||||||
|
|
||||||
|
-- Comentários para documentação
|
||||||
|
COMMENT ON COLUMN vendas.status_pagamento IS 'Status do pagamento: pendente, pago, cancelado, expirado';
|
||||||
|
COMMENT ON COLUMN vendas.data_pagamento IS 'Data e hora da confirmação do pagamento';
|
||||||
|
COMMENT ON COLUMN vendas.pix_payment_id IS 'ID do pagamento no Mercado Pago';
|
||||||
|
COMMENT ON COLUMN vendas.pix_qr_code IS 'Código PIX para copiar e colar';
|
||||||
|
COMMENT ON COLUMN vendas.metodo_pagamento IS 'Método: dinheiro, cartao, pix, transferencia';
|
||||||
|
|
||||||
|
-- Verificar se as colunas foram criadas
|
||||||
|
SELECT column_name, data_type, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'vendas'
|
||||||
|
AND column_name IN ('status_pagamento', 'data_pagamento', 'pix_payment_id', 'pix_qr_code', 'metodo_pagamento')
|
||||||
|
ORDER BY column_name;
|
||||||
159
backup-projeto-completo.sh
Executable file
159
backup-projeto-completo.sh
Executable file
@@ -0,0 +1,159 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 💾 Backup COMPLETO do Projeto Liberi Kids
|
||||||
|
# Cria backup de tudo antes de fazer deploy
|
||||||
|
# Execute: ./backup-projeto-completo.sh
|
||||||
|
|
||||||
|
echo "💾 BACKUP COMPLETO - Liberi Kids"
|
||||||
|
echo "================================"
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[✅ OK]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[⚠️ WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo -e "${BLUE}[ℹ️ INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[❌ ERRO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configurações
|
||||||
|
BACKUP_DIR="$HOME/liberi-backups"
|
||||||
|
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
|
BACKUP_NAME="liberi-kids-backup-$TIMESTAMP"
|
||||||
|
BACKUP_PATH="$BACKUP_DIR/$BACKUP_NAME"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "📋 ESTE BACKUP INCLUIRÁ:"
|
||||||
|
echo " 📁 Código fonte completo"
|
||||||
|
echo " 🔧 Configurações (.env)"
|
||||||
|
echo " 📦 Frontend compilado"
|
||||||
|
echo " 🗄️ Scripts e documentação"
|
||||||
|
echo " 🏦 Configurações PIX"
|
||||||
|
echo " 📚 Todos os arquivos do projeto"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Criar diretório de backup
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "💾 CRIANDO BACKUP COMPLETO..."
|
||||||
|
|
||||||
|
# Criar backup
|
||||||
|
cp -r . $BACKUP_PATH
|
||||||
|
|
||||||
|
# Remover arquivos desnecessários do backup
|
||||||
|
rm -rf $BACKUP_PATH/node_modules 2>/dev/null || true
|
||||||
|
rm -rf $BACKUP_PATH/client/node_modules 2>/dev/null || true
|
||||||
|
rm -rf $BACKUP_PATH/.git 2>/dev/null || true
|
||||||
|
rm -rf $BACKUP_PATH/logs 2>/dev/null || true
|
||||||
|
rm -rf $BACKUP_PATH/uploads/* 2>/dev/null || true
|
||||||
|
|
||||||
|
log "✅ Backup criado: $BACKUP_PATH"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "📊 ESTATÍSTICAS DO BACKUP:"
|
||||||
|
|
||||||
|
# Contar arquivos
|
||||||
|
TOTAL_FILES=$(find $BACKUP_PATH -type f | wc -l)
|
||||||
|
TOTAL_SIZE=$(du -sh $BACKUP_PATH | cut -f1)
|
||||||
|
|
||||||
|
echo " 📁 Total de arquivos: $TOTAL_FILES"
|
||||||
|
echo " 💾 Tamanho total: $TOTAL_SIZE"
|
||||||
|
echo " 📅 Data: $(date)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "📋 CONTEÚDO DO BACKUP:"
|
||||||
|
echo " ✅ server-supabase.js - Servidor principal"
|
||||||
|
echo " ✅ client/ - Frontend React"
|
||||||
|
echo " ✅ config/ - Configurações (Supabase, PIX, Google)"
|
||||||
|
echo " ✅ package.json - Dependências"
|
||||||
|
echo " ✅ .env - Credenciais (se existir)"
|
||||||
|
echo " ✅ *.sh - Scripts de deploy"
|
||||||
|
echo " ✅ *.md - Documentação"
|
||||||
|
echo " ✅ *.sql - Scripts de banco"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Criar arquivo de informações do backup
|
||||||
|
cat > $BACKUP_PATH/BACKUP-INFO.txt << EOF
|
||||||
|
BACKUP LIBERI KIDS
|
||||||
|
==================
|
||||||
|
|
||||||
|
Data: $(date)
|
||||||
|
Versão: Completa com PIX
|
||||||
|
Servidor: Supabase
|
||||||
|
Frontend: React compilado
|
||||||
|
|
||||||
|
CONTEÚDO:
|
||||||
|
- Código fonte completo
|
||||||
|
- Configurações PIX Mercado Pago
|
||||||
|
- Frontend compilado
|
||||||
|
- Scripts de deploy
|
||||||
|
- Documentação completa
|
||||||
|
|
||||||
|
RESTAURAR:
|
||||||
|
1. Extrair backup
|
||||||
|
2. npm install
|
||||||
|
3. Configurar .env
|
||||||
|
4. ./deploy-completo-servidor.sh
|
||||||
|
|
||||||
|
SUPORTE:
|
||||||
|
- Banco: Supabase (não incluído no backup)
|
||||||
|
- PIX: Mercado Pago
|
||||||
|
- Deploy: Scripts automatizados
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "✅ Arquivo de informações criado"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🗜️ COMPACTANDO BACKUP..."
|
||||||
|
|
||||||
|
# Criar arquivo compactado
|
||||||
|
cd $BACKUP_DIR
|
||||||
|
tar -czf "$BACKUP_NAME.tar.gz" "$BACKUP_NAME"
|
||||||
|
rm -rf "$BACKUP_NAME"
|
||||||
|
|
||||||
|
COMPRESSED_SIZE=$(du -sh "$BACKUP_NAME.tar.gz" | cut -f1)
|
||||||
|
|
||||||
|
log "✅ Backup compactado: $BACKUP_DIR/$BACKUP_NAME.tar.gz"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "🎉 BACKUP COMPLETO FINALIZADO!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "📋 RESUMO DO BACKUP:"
|
||||||
|
echo " 📁 Localização: $BACKUP_DIR/$BACKUP_NAME.tar.gz"
|
||||||
|
echo " 💾 Tamanho: $COMPRESSED_SIZE"
|
||||||
|
echo " 📅 Data: $(date)"
|
||||||
|
echo " 🔢 Arquivos: $TOTAL_FILES"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "🔄 COMO RESTAURAR:"
|
||||||
|
echo " 1. cd $BACKUP_DIR"
|
||||||
|
echo " 2. tar -xzf $BACKUP_NAME.tar.gz"
|
||||||
|
echo " 3. cd $BACKUP_NAME"
|
||||||
|
echo " 4. npm install"
|
||||||
|
echo " 5. ./deploy-completo-servidor.sh"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "📂 BACKUPS DISPONÍVEIS:"
|
||||||
|
ls -la $BACKUP_DIR/*.tar.gz 2>/dev/null | tail -5 || echo " (Este é o primeiro backup)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
warn "💡 DICA: Mantenha backups regulares"
|
||||||
|
warn "💡 O banco Supabase não está incluído (é na nuvem)"
|
||||||
|
|
||||||
|
log "✨ BACKUP SEGURO CRIADO!"
|
||||||
20879
client/package-lock.json
generated
Normal file
20879
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
client/package.json
Normal file
45
client/package.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "liberi-kids-client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^14.5.2",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.15.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"axios": "^1.5.0",
|
||||||
|
"react-icons": "^4.11.0",
|
||||||
|
"recharts": "^2.8.0",
|
||||||
|
"react-hook-form": "^7.46.1",
|
||||||
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"proxy": "http://localhost:5000"
|
||||||
|
}
|
||||||
2693
client/src/App.css
Normal file
2693
client/src/App.css
Normal file
File diff suppressed because it is too large
Load Diff
51
client/src/App.js
Normal file
51
client/src/App.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
import Layout from './components/Layout';
|
||||||
|
import { NotificationProvider } from './components/NotificationCenter';
|
||||||
|
import Dashboard from './pages/DashboardSimples';
|
||||||
|
import Produtos from './pages/Produtos';
|
||||||
|
import Clientes from './pages/Clientes';
|
||||||
|
import Fornecedores from './pages/Fornecedores';
|
||||||
|
import Despesas from './pages/Despesas';
|
||||||
|
import Vendas from './pages/Vendas';
|
||||||
|
import Devolucoes from './pages/Devolucoes';
|
||||||
|
import Emprestimos from './pages/Emprestimos';
|
||||||
|
import Configuracoes from './pages/Configuracoes';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<NotificationProvider>
|
||||||
|
<div className="App">
|
||||||
|
<Toaster
|
||||||
|
position="top-right"
|
||||||
|
toastOptions={{
|
||||||
|
duration: 4000,
|
||||||
|
style: {
|
||||||
|
background: '#363636',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Layout>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Dashboard />} />
|
||||||
|
<Route path="/produtos" element={<Produtos />} />
|
||||||
|
<Route path="/clientes" element={<Clientes />} />
|
||||||
|
<Route path="/fornecedores" element={<Fornecedores />} />
|
||||||
|
<Route path="/despesas" element={<Despesas />} />
|
||||||
|
<Route path="/vendas" element={<Vendas />} />
|
||||||
|
<Route path="/devolucoes" element={<Devolucoes />} />
|
||||||
|
<Route path="/emprestimos" element={<Emprestimos />} />
|
||||||
|
<Route path="/configuracoes" element={<Configuracoes />} />
|
||||||
|
</Routes>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</NotificationProvider>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
343
client/src/components/ChatWhatsApp.css
Normal file
343
client/src/components/ChatWhatsApp.css
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
/* Chat WhatsApp Overlay */
|
||||||
|
.chat-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
height: 600px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.chat-header {
|
||||||
|
background: #075e54;
|
||||||
|
color: white;
|
||||||
|
padding: 16px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-client-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-client-details h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-phone {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-close-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Messages Area */
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
background: #e5ddd5;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23d4d4d4' fill-opacity='0.1'%3E%3Cpath d='M20 20c0 11.046-8.954 20-20 20v-40c11.046 0 20 8.954 20 20z'/%3E%3C/g%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid #e0e0e0;
|
||||||
|
border-top: 3px solid #25d366;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-empty svg {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-empty p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-empty span {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date Divider */
|
||||||
|
.chat-date-divider {
|
||||||
|
text-align: center;
|
||||||
|
margin: 16px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-date-divider::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-date-divider::after {
|
||||||
|
content: attr(data-date);
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Messages */
|
||||||
|
.chat-message {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.enviada {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.recebida {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
max-width: 70%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.enviada .message-content {
|
||||||
|
background: #dcf8c6;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.recebida .message-content {
|
||||||
|
background: white;
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content p {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-status {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-status.enviando {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-status.enviada {
|
||||||
|
color: #4fc3f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-status.entregue {
|
||||||
|
color: #4fc3f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-status.lida {
|
||||||
|
color: #25d366;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Area */
|
||||||
|
.chat-input-area {
|
||||||
|
background: white;
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
max-height: 100px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input::placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-send-btn {
|
||||||
|
background: #25d366;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-send-btn:hover:not(:disabled) {
|
||||||
|
background: #128c7e;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-send-btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chat-overlay {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
height: 100vh;
|
||||||
|
max-width: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-area {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar personalizada */
|
||||||
|
.chat-messages::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
218
client/src/components/ChatWhatsApp.js
Normal file
218
client/src/components/ChatWhatsApp.js
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { FiX, FiSend, FiPhone, FiMessageCircle } from 'react-icons/fi';
|
||||||
|
import './ChatWhatsApp.css';
|
||||||
|
|
||||||
|
const ChatWhatsApp = ({ isOpen, onClose, cliente }) => {
|
||||||
|
const [mensagens, setMensagens] = useState([]);
|
||||||
|
const [novaMensagem, setNovaMensagem] = useState('');
|
||||||
|
const [enviando, setEnviando] = useState(false);
|
||||||
|
const [carregando, setCarregando] = useState(false);
|
||||||
|
const messagesEndRef = useRef(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [mensagens]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && cliente?.telefone) {
|
||||||
|
carregarHistorico();
|
||||||
|
}
|
||||||
|
}, [isOpen, cliente]);
|
||||||
|
|
||||||
|
const carregarHistorico = async () => {
|
||||||
|
setCarregando(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/chat/${cliente.telefone}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setMensagens(data.data || []);
|
||||||
|
} else {
|
||||||
|
console.error('Erro ao carregar histórico:', data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar histórico:', error);
|
||||||
|
} finally {
|
||||||
|
setCarregando(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const enviarMensagem = async () => {
|
||||||
|
if (!novaMensagem.trim() || enviando) return;
|
||||||
|
|
||||||
|
setEnviando(true);
|
||||||
|
const mensagemTemp = {
|
||||||
|
id: Date.now(),
|
||||||
|
mensagem: novaMensagem,
|
||||||
|
tipo: 'enviada',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
status: 'enviando'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adicionar mensagem temporária
|
||||||
|
setMensagens(prev => [...prev, mensagemTemp]);
|
||||||
|
setNovaMensagem('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/chat/enviar', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
telefone: cliente.telefone,
|
||||||
|
mensagem: novaMensagem,
|
||||||
|
clienteNome: cliente.nome
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Atualizar mensagem temporária com dados reais
|
||||||
|
setMensagens(prev =>
|
||||||
|
prev.map(msg =>
|
||||||
|
msg.id === mensagemTemp.id
|
||||||
|
? { ...data.data, status: 'enviada' }
|
||||||
|
: msg
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Remover mensagem temporária e mostrar erro
|
||||||
|
setMensagens(prev => prev.filter(msg => msg.id !== mensagemTemp.id));
|
||||||
|
alert(`Erro ao enviar mensagem: ${data.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao enviar mensagem:', error);
|
||||||
|
setMensagens(prev => prev.filter(msg => msg.id !== mensagemTemp.id));
|
||||||
|
alert('Erro ao enviar mensagem. Tente novamente.');
|
||||||
|
} finally {
|
||||||
|
setEnviando(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
enviarMensagem();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatarHora = (timestamp) => {
|
||||||
|
return new Date(timestamp).toLocaleTimeString('pt-BR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatarData = (timestamp) => {
|
||||||
|
return new Date(timestamp).toLocaleDateString('pt-BR');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chat-overlay">
|
||||||
|
<div className="chat-container">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="chat-header">
|
||||||
|
<div className="chat-client-info">
|
||||||
|
<div className="chat-avatar">
|
||||||
|
<FiMessageCircle size={24} />
|
||||||
|
</div>
|
||||||
|
<div className="chat-client-details">
|
||||||
|
<h3>{cliente?.nome || 'Cliente'}</h3>
|
||||||
|
<span className="chat-phone">
|
||||||
|
<FiPhone size={14} />
|
||||||
|
{cliente?.telefone}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="chat-close-btn" onClick={onClose}>
|
||||||
|
<FiX size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages Area */}
|
||||||
|
<div className="chat-messages">
|
||||||
|
{carregando ? (
|
||||||
|
<div className="chat-loading">
|
||||||
|
<div className="loading-spinner"></div>
|
||||||
|
<p>Carregando histórico...</p>
|
||||||
|
</div>
|
||||||
|
) : mensagens.length === 0 ? (
|
||||||
|
<div className="chat-empty">
|
||||||
|
<FiMessageCircle size={48} />
|
||||||
|
<p>Nenhuma mensagem ainda</p>
|
||||||
|
<span>Inicie uma conversa com {cliente?.nome}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{mensagens.map((mensagem, index) => {
|
||||||
|
const showDate = index === 0 ||
|
||||||
|
formatarData(mensagem.created_at) !== formatarData(mensagens[index - 1].created_at);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={mensagem.id || index}>
|
||||||
|
{showDate && (
|
||||||
|
<div className="chat-date-divider">
|
||||||
|
{formatarData(mensagem.created_at)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`chat-message ${mensagem.tipo}`}>
|
||||||
|
<div className="message-content">
|
||||||
|
<p>{mensagem.mensagem}</p>
|
||||||
|
<div className="message-info">
|
||||||
|
<span className="message-time">
|
||||||
|
{formatarHora(mensagem.created_at)}
|
||||||
|
</span>
|
||||||
|
{mensagem.tipo === 'enviada' && (
|
||||||
|
<span className={`message-status ${mensagem.status}`}>
|
||||||
|
{mensagem.status === 'enviando' ? '⏳' :
|
||||||
|
mensagem.status === 'enviada' ? '✓' :
|
||||||
|
mensagem.status === 'entregue' ? '✓✓' :
|
||||||
|
mensagem.status === 'lida' ? '✓✓' : ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Area */}
|
||||||
|
<div className="chat-input-area">
|
||||||
|
<div className="chat-input-container">
|
||||||
|
<textarea
|
||||||
|
value={novaMensagem}
|
||||||
|
onChange={(e) => setNovaMensagem(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
placeholder="Digite sua mensagem..."
|
||||||
|
className="chat-input"
|
||||||
|
rows="1"
|
||||||
|
disabled={enviando}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={enviarMensagem}
|
||||||
|
disabled={!novaMensagem.trim() || enviando}
|
||||||
|
className="chat-send-btn"
|
||||||
|
>
|
||||||
|
<FiSend size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatWhatsApp;
|
||||||
325
client/src/components/Layout.css
Normal file
325
client/src/components/Layout.css
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background: linear-gradient(180deg, #1e293b 0%, #334155 100%);
|
||||||
|
color: white;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 24px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 12px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-active {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
border-left-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 999;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: white;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-toggle {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #374151;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-toggle:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.sidebar {
|
||||||
|
position: static;
|
||||||
|
transform: translateX(0);
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 1023px) and (min-width: 768px) {
|
||||||
|
.sidebar-overlay {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-overlay {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title p {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
STATUS DE CONEXÃO
|
||||||
|
===================================================== */
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-text {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-connected {
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
color: #10b981;
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-disconnected {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #ef4444;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-error {
|
||||||
|
background: rgba(245, 158, 11, 0.1);
|
||||||
|
color: #f59e0b;
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animação de pulso para status offline */
|
||||||
|
.connection-disconnected,
|
||||||
|
.connection-error {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
126
client/src/components/Layout.js
Normal file
126
client/src/components/Layout.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
FiHome,
|
||||||
|
FiPackage,
|
||||||
|
FiUsers,
|
||||||
|
FiTruck,
|
||||||
|
FiDollarSign,
|
||||||
|
FiShoppingCart,
|
||||||
|
FiRotateCcw,
|
||||||
|
FiSettings,
|
||||||
|
FiMenu,
|
||||||
|
FiX,
|
||||||
|
FiWifi,
|
||||||
|
FiWifiOff,
|
||||||
|
FiCreditCard
|
||||||
|
} from 'react-icons/fi';
|
||||||
|
import './Layout.css';
|
||||||
|
|
||||||
|
const Layout = ({ children }) => {
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ path: '/', icon: FiHome, label: 'Dashboard' },
|
||||||
|
{ path: '/produtos', icon: FiPackage, label: 'Produtos' },
|
||||||
|
{ path: '/clientes', icon: FiUsers, label: 'Clientes' },
|
||||||
|
{ path: '/fornecedores', icon: FiTruck, label: 'Fornecedores' },
|
||||||
|
{ path: '/despesas', icon: FiDollarSign, label: 'Despesas' },
|
||||||
|
{ path: '/vendas', icon: FiShoppingCart, label: 'Vendas' },
|
||||||
|
{ path: '/devolucoes', icon: FiRotateCcw, label: 'Devolução/Troca' },
|
||||||
|
{ path: '/emprestimos', icon: FiCreditCard, label: 'Empréstimos' },
|
||||||
|
{ path: '/configuracoes', icon: FiSettings, label: 'Configurações' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
setSidebarOpen(!sidebarOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="layout">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside className={`sidebar ${sidebarOpen ? 'sidebar-open' : ''}`}>
|
||||||
|
<div className="sidebar-header">
|
||||||
|
<div className="logo">
|
||||||
|
<img
|
||||||
|
src="/LogoLiberiKids.png"
|
||||||
|
alt="Liberi Kids"
|
||||||
|
className="logo-img"
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.style.display = 'none';
|
||||||
|
e.target.nextSibling.style.display = 'block';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="logo-text" style={{ display: 'none' }}>
|
||||||
|
<h2>Liberi Kids</h2>
|
||||||
|
<span>Moda Infantil</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="sidebar-close"
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
>
|
||||||
|
<FiX />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="sidebar-nav">
|
||||||
|
{menuItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
const isActive = location.pathname === item.path;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
className={`nav-item ${isActive ? 'nav-item-active' : ''}`}
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
>
|
||||||
|
<Icon className="nav-icon" />
|
||||||
|
<span className="nav-label">{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Overlay para mobile */}
|
||||||
|
{sidebarOpen && (
|
||||||
|
<div
|
||||||
|
className="sidebar-overlay"
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="main-content">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="header">
|
||||||
|
<button
|
||||||
|
className="menu-toggle"
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
>
|
||||||
|
<FiMenu />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="header-title">
|
||||||
|
<h1>Sistema de Controle de Estoque</h1>
|
||||||
|
<p>Liberi Kids - Moda Infantil</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="header-actions">
|
||||||
|
{/* Espaço para futuras ações */}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Page Content */}
|
||||||
|
<main className="page-content">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
263
client/src/components/NotificationCenter.css
Normal file
263
client/src/components/NotificationCenter.css
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
/* =====================================================
|
||||||
|
CENTRO DE NOTIFICAÇÕES
|
||||||
|
===================================================== */
|
||||||
|
|
||||||
|
.notification-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 16px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1), 0 4px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
border-left: 4px solid;
|
||||||
|
pointer-events: all;
|
||||||
|
cursor: pointer;
|
||||||
|
transform: translateX(100%);
|
||||||
|
animation: slideIn 0.3s ease forwards;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15), 0 6px 15px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-success {
|
||||||
|
border-left-color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-error {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-warning {
|
||||||
|
border-left-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-info {
|
||||||
|
border-left-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-success .notification-icon {
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-error .notification-icon {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-warning .notification-icon {
|
||||||
|
background: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-info .notification-icon {
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-message {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-close {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-close:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
INDICADOR DE CONEXÃO
|
||||||
|
===================================================== */
|
||||||
|
|
||||||
|
.connection-indicator {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 25px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
z-index: 9998;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-icon.connected {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-icon.disconnected {
|
||||||
|
color: #ef4444;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-icon.error {
|
||||||
|
color: #f59e0b;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-connected {
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||||
|
background: rgba(16, 185, 129, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-disconnected {
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
|
background: rgba(239, 68, 68, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-error {
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||||
|
background: rgba(245, 158, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-connected .connection-text {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-disconnected .connection-text {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-error .connection-text {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
ANIMAÇÕES
|
||||||
|
===================================================== */
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
RESPONSIVO
|
||||||
|
===================================================== */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.notification-container {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
min-width: auto;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-indicator {
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-indicator {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.notification {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-message {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-time {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
190
client/src/components/NotificationCenter.js
Normal file
190
client/src/components/NotificationCenter.js
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import React, { useState, useEffect, createContext, useContext } from 'react';
|
||||||
|
import { FiCheck, FiX, FiAlertTriangle, FiInfo, FiWifi, FiWifiOff } from 'react-icons/fi';
|
||||||
|
import './NotificationCenter.css';
|
||||||
|
|
||||||
|
// Context para notificações globais
|
||||||
|
const NotificationContext = createContext();
|
||||||
|
|
||||||
|
export const useNotification = () => {
|
||||||
|
const context = useContext(NotificationContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useNotification deve ser usado dentro de NotificationProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Provider de notificações
|
||||||
|
export const NotificationProvider = ({ children }) => {
|
||||||
|
const [notifications, setNotifications] = useState([]);
|
||||||
|
const [connectionStatus, setConnectionStatus] = useState('connected');
|
||||||
|
|
||||||
|
// Adicionar notificação
|
||||||
|
const addNotification = (message, type = 'info', duration = 5000) => {
|
||||||
|
const id = Date.now() + Math.random();
|
||||||
|
const notification = {
|
||||||
|
id,
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
timestamp: new Date(),
|
||||||
|
duration
|
||||||
|
};
|
||||||
|
|
||||||
|
setNotifications(prev => [...prev, notification]);
|
||||||
|
|
||||||
|
// Remover automaticamente após o tempo especificado
|
||||||
|
if (duration > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
removeNotification(id);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remover notificação
|
||||||
|
const removeNotification = (id) => {
|
||||||
|
setNotifications(prev => prev.filter(n => n.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notificações de conveniência
|
||||||
|
const success = (message, duration) => addNotification(message, 'success', duration);
|
||||||
|
const error = (message, duration) => addNotification(message, 'error', duration);
|
||||||
|
const warning = (message, duration) => addNotification(message, 'warning', duration);
|
||||||
|
const info = (message, duration) => addNotification(message, 'info', duration);
|
||||||
|
|
||||||
|
// Monitorar conexão
|
||||||
|
useEffect(() => {
|
||||||
|
const checkConnection = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/dashboard', {
|
||||||
|
method: 'GET',
|
||||||
|
signal: AbortSignal.timeout(5000)
|
||||||
|
});
|
||||||
|
|
||||||
|
const newStatus = response.ok ? 'connected' : 'error';
|
||||||
|
|
||||||
|
// Notificar mudanças de status
|
||||||
|
if (connectionStatus !== newStatus) {
|
||||||
|
if (newStatus === 'connected' && connectionStatus !== 'connected') {
|
||||||
|
success('Conexão restaurada!', 3000);
|
||||||
|
} else if (newStatus === 'error') {
|
||||||
|
warning('Problemas de conexão detectados', 4000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setConnectionStatus(newStatus);
|
||||||
|
} catch (error) {
|
||||||
|
const newStatus = 'disconnected';
|
||||||
|
|
||||||
|
if (connectionStatus !== newStatus) {
|
||||||
|
error('Conexão perdida com o servidor', 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setConnectionStatus(newStatus);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verificar imediatamente
|
||||||
|
checkConnection();
|
||||||
|
|
||||||
|
// Verificar a cada 30 segundos
|
||||||
|
const interval = setInterval(checkConnection, 30000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [connectionStatus]);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
notifications,
|
||||||
|
connectionStatus,
|
||||||
|
addNotification,
|
||||||
|
removeNotification,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
warning,
|
||||||
|
info
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
<NotificationCenter />
|
||||||
|
</NotificationContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Componente de notificações
|
||||||
|
const NotificationCenter = () => {
|
||||||
|
const { notifications, removeNotification, connectionStatus } = useNotification();
|
||||||
|
|
||||||
|
const getIcon = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'success': return <FiCheck />;
|
||||||
|
case 'error': return <FiX />;
|
||||||
|
case 'warning': return <FiAlertTriangle />;
|
||||||
|
default: return <FiInfo />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConnectionIcon = () => {
|
||||||
|
switch (connectionStatus) {
|
||||||
|
case 'connected': return <FiWifi className="connection-icon connected" />;
|
||||||
|
case 'disconnected': return <FiWifiOff className="connection-icon disconnected" />;
|
||||||
|
case 'error': return <FiWifiOff className="connection-icon error" />;
|
||||||
|
default: return <FiWifi className="connection-icon" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConnectionText = () => {
|
||||||
|
switch (connectionStatus) {
|
||||||
|
case 'connected': return 'Online';
|
||||||
|
case 'disconnected': return 'Offline';
|
||||||
|
case 'error': return 'Instável';
|
||||||
|
default: return 'Verificando...';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Status de conexão fixo */}
|
||||||
|
<div className={`connection-indicator connection-${connectionStatus}`}>
|
||||||
|
{getConnectionIcon()}
|
||||||
|
<span className="connection-text">{getConnectionText()}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Container de notificações */}
|
||||||
|
<div className="notification-container">
|
||||||
|
{notifications.map((notification) => (
|
||||||
|
<div
|
||||||
|
key={notification.id}
|
||||||
|
className={`notification notification-${notification.type}`}
|
||||||
|
onClick={() => removeNotification(notification.id)}
|
||||||
|
>
|
||||||
|
<div className="notification-icon">
|
||||||
|
{getIcon(notification.type)}
|
||||||
|
</div>
|
||||||
|
<div className="notification-content">
|
||||||
|
<p className="notification-message">{notification.message}</p>
|
||||||
|
<span className="notification-time">
|
||||||
|
{notification.timestamp.toLocaleTimeString('pt-BR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="notification-close"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeNotification(notification.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiX />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationCenter;
|
||||||
25
client/src/components/ViewToggle.js
Normal file
25
client/src/components/ViewToggle.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FiGrid, FiList } from 'react-icons/fi';
|
||||||
|
|
||||||
|
const ViewToggle = ({ viewMode, onViewModeChange }) => {
|
||||||
|
return (
|
||||||
|
<div className="view-toggle">
|
||||||
|
<button
|
||||||
|
className={`view-toggle-btn ${viewMode === 'cards' ? 'active' : ''}`}
|
||||||
|
onClick={() => onViewModeChange('cards')}
|
||||||
|
title="Visualização em Cards"
|
||||||
|
>
|
||||||
|
<FiGrid />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`view-toggle-btn ${viewMode === 'list' ? 'active' : ''}`}
|
||||||
|
onClick={() => onViewModeChange('list')}
|
||||||
|
title="Visualização em Lista"
|
||||||
|
>
|
||||||
|
<FiList />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewToggle;
|
||||||
134
client/src/index.css
Normal file
134
client/src/index.css
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar personalizada */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animações */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utilitários */
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-1 { margin-bottom: 0.25rem; }
|
||||||
|
.mb-2 { margin-bottom: 0.5rem; }
|
||||||
|
.mb-3 { margin-bottom: 0.75rem; }
|
||||||
|
.mb-4 { margin-bottom: 1rem; }
|
||||||
|
.mb-6 { margin-bottom: 1.5rem; }
|
||||||
|
.mb-8 { margin-bottom: 2rem; }
|
||||||
|
|
||||||
|
.mt-1 { margin-top: 0.25rem; }
|
||||||
|
.mt-2 { margin-top: 0.5rem; }
|
||||||
|
.mt-3 { margin-top: 0.75rem; }
|
||||||
|
.mt-4 { margin-top: 1rem; }
|
||||||
|
.mt-6 { margin-top: 1.5rem; }
|
||||||
|
.mt-8 { margin-top: 2rem; }
|
||||||
|
|
||||||
|
.p-2 { padding: 0.5rem; }
|
||||||
|
.p-3 { padding: 0.75rem; }
|
||||||
|
.p-4 { padding: 1rem; }
|
||||||
|
.p-6 { padding: 1.5rem; }
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-4 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-full {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-lg {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow {
|
||||||
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-lg {
|
||||||
|
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
11
client/src/index.js
Normal file
11
client/src/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
411
client/src/pages/Clientes.js
Normal file
411
client/src/pages/Clientes.js
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
FiPlus,
|
||||||
|
FiEdit,
|
||||||
|
FiTrash2,
|
||||||
|
FiSearch,
|
||||||
|
FiUsers,
|
||||||
|
FiMail,
|
||||||
|
FiPhone,
|
||||||
|
FiMapPin,
|
||||||
|
FiMessageSquare
|
||||||
|
} from 'react-icons/fi';
|
||||||
|
import { clientesAPI } from '../services/api';
|
||||||
|
import ViewToggle from '../components/ViewToggle';
|
||||||
|
import ChatWhatsApp from '../components/ChatWhatsApp';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
const Clientes = () => {
|
||||||
|
const [clientes, setClientes] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [viewMode, setViewMode] = useState('list');
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [editingClient, setEditingClient] = useState(null);
|
||||||
|
const [showChatModal, setShowChatModal] = useState(false);
|
||||||
|
const [selectedCliente, setSelectedCliente] = useState(null);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
nome_completo: '',
|
||||||
|
email: '',
|
||||||
|
whatsapp: '',
|
||||||
|
endereco: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
carregarClientes();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const carregarClientes = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await clientesAPI.listar();
|
||||||
|
setClientes(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar clientes:', error);
|
||||||
|
toast.error('Erro ao carregar clientes');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
if (editingClient) {
|
||||||
|
await clientesAPI.atualizar(editingClient.id, formData);
|
||||||
|
toast.success('Cliente atualizado com sucesso!');
|
||||||
|
} else {
|
||||||
|
await clientesAPI.criar(formData);
|
||||||
|
toast.success('Cliente cadastrado com sucesso!');
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowModal(false);
|
||||||
|
setEditingClient(null);
|
||||||
|
setFormData({
|
||||||
|
nome_completo: '',
|
||||||
|
email: '',
|
||||||
|
whatsapp: '',
|
||||||
|
endereco: ''
|
||||||
|
});
|
||||||
|
carregarClientes();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao salvar cliente:', error);
|
||||||
|
toast.error('Erro ao salvar cliente');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (cliente) => {
|
||||||
|
setEditingClient(cliente);
|
||||||
|
setFormData({
|
||||||
|
nome_completo: cliente.nome_completo,
|
||||||
|
email: cliente.email || '',
|
||||||
|
whatsapp: cliente.whatsapp || '',
|
||||||
|
endereco: cliente.endereco || ''
|
||||||
|
});
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (window.confirm('Tem certeza que deseja excluir este cliente?')) {
|
||||||
|
try {
|
||||||
|
await clientesAPI.deletar(id);
|
||||||
|
toast.success('Cliente excluído com sucesso!');
|
||||||
|
carregarClientes();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao excluir cliente:', error);
|
||||||
|
toast.error('Erro ao excluir cliente');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const abrirChat = (cliente) => {
|
||||||
|
if (!cliente.whatsapp) {
|
||||||
|
toast.error('Cliente não possui WhatsApp cadastrado');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clienteFormatado = {
|
||||||
|
nome: cliente.nome_completo,
|
||||||
|
telefone: cliente.whatsapp.replace(/\D/g, ''),
|
||||||
|
id: cliente.id
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedCliente(clienteFormatado);
|
||||||
|
setShowChatModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredClientes = clientes.filter(cliente =>
|
||||||
|
cliente.nome_completo.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(cliente.email && cliente.email.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||||
|
(cliente.whatsapp && cliente.whatsapp.includes(searchTerm)) ||
|
||||||
|
(cliente.id_cliente && cliente.id_cliente.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="loading">
|
||||||
|
<div>Carregando clientes...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="clientes fade-in">
|
||||||
|
<div className="page-header">
|
||||||
|
<div>
|
||||||
|
<h1>Clientes</h1>
|
||||||
|
<p>Gerencie os clientes da loja</p>
|
||||||
|
</div>
|
||||||
|
<div className="header-actions">
|
||||||
|
<ViewToggle
|
||||||
|
viewMode={viewMode}
|
||||||
|
onViewModeChange={setViewMode}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
>
|
||||||
|
<FiPlus />
|
||||||
|
Novo Cliente
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Barra de Pesquisa */}
|
||||||
|
<div className="search-box">
|
||||||
|
<FiSearch className="search-icon" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Pesquisar clientes..."
|
||||||
|
className="search-input"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lista de Clientes */}
|
||||||
|
{filteredClientes.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<FiUsers size={48} />
|
||||||
|
<h3>Nenhum cliente encontrado</h3>
|
||||||
|
<p>Comece adicionando seu primeiro cliente</p>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
>
|
||||||
|
<FiPlus />
|
||||||
|
Adicionar Cliente
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : viewMode === 'cards' ? (
|
||||||
|
<div className="clients-grid">
|
||||||
|
{filteredClientes.map((cliente) => (
|
||||||
|
<div key={cliente.id} className="client-card">
|
||||||
|
<div className="client-header">
|
||||||
|
<div className="client-avatar">
|
||||||
|
<FiUsers />
|
||||||
|
</div>
|
||||||
|
<div className="client-actions">
|
||||||
|
{cliente.whatsapp && (
|
||||||
|
<button
|
||||||
|
className="btn-icon btn-success"
|
||||||
|
onClick={() => abrirChat(cliente)}
|
||||||
|
title="Abrir Chat WhatsApp"
|
||||||
|
>
|
||||||
|
<FiMessageSquare />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="btn-icon"
|
||||||
|
onClick={() => handleEdit(cliente)}
|
||||||
|
title="Editar"
|
||||||
|
>
|
||||||
|
<FiEdit />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-icon btn-danger"
|
||||||
|
onClick={() => handleDelete(cliente.id)}
|
||||||
|
title="Excluir"
|
||||||
|
>
|
||||||
|
<FiTrash2 />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="client-info">
|
||||||
|
<h3 className="client-name">{cliente.nome_completo}</h3>
|
||||||
|
|
||||||
|
{cliente.id_cliente && (
|
||||||
|
<div className="client-detail">
|
||||||
|
<span className="client-id">#{cliente.id_cliente}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cliente.email && (
|
||||||
|
<div className="client-detail">
|
||||||
|
<FiMail className="detail-icon" />
|
||||||
|
<span>{cliente.email}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cliente.whatsapp && (
|
||||||
|
<div className="client-detail">
|
||||||
|
<FiPhone className="detail-icon" />
|
||||||
|
<span>{cliente.whatsapp}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cliente.endereco && (
|
||||||
|
<div className="client-detail">
|
||||||
|
<FiMapPin className="detail-icon" />
|
||||||
|
<span>{cliente.endereco}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="client-meta">
|
||||||
|
<span className="client-date">
|
||||||
|
Cadastrado em {new Date(cliente.created_at).toLocaleDateString('pt-BR')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="list-view">
|
||||||
|
<div className="list-header clientes-list-header">
|
||||||
|
<span>ID / Nome</span>
|
||||||
|
<span>Email</span>
|
||||||
|
<span>WhatsApp</span>
|
||||||
|
<span>Endereço</span>
|
||||||
|
<span>Ações</span>
|
||||||
|
</div>
|
||||||
|
{filteredClientes.map((cliente) => (
|
||||||
|
<div key={cliente.id} className="list-item clientes-list-item">
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: '600' }}>
|
||||||
|
{cliente.id_cliente && <span style={{ color: '#3b82f6', fontSize: '12px', fontWeight: '700' }}>#{cliente.id_cliente} - </span>}
|
||||||
|
{cliente.nome_completo}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#6b7280' }}>
|
||||||
|
Cadastrado em {new Date(cliente.created_at).toLocaleDateString('pt-BR')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>{cliente.email || '-'}</span>
|
||||||
|
<span>{cliente.whatsapp || '-'}</span>
|
||||||
|
<span title={cliente.endereco} style={{ maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
|
{cliente.endereco || '-'}
|
||||||
|
</span>
|
||||||
|
<div className="list-item-actions">
|
||||||
|
{cliente.whatsapp && (
|
||||||
|
<button
|
||||||
|
className="btn-icon btn-sm btn-success"
|
||||||
|
onClick={() => abrirChat(cliente)}
|
||||||
|
title="Abrir Chat WhatsApp"
|
||||||
|
>
|
||||||
|
<FiMessageSquare />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="btn-icon btn-sm"
|
||||||
|
onClick={() => handleEdit(cliente)}
|
||||||
|
title="Editar"
|
||||||
|
>
|
||||||
|
<FiEdit />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-icon btn-sm btn-danger"
|
||||||
|
onClick={() => handleDelete(cliente.id)}
|
||||||
|
title="Excluir"
|
||||||
|
>
|
||||||
|
<FiTrash2 />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal de Cliente */}
|
||||||
|
{showModal && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2 className="modal-title">
|
||||||
|
{editingClient ? 'Editar Cliente' : 'Novo Cliente'}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
className="modal-close"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setEditingClient(null);
|
||||||
|
setFormData({
|
||||||
|
nome_completo: '',
|
||||||
|
email: '',
|
||||||
|
whatsapp: '',
|
||||||
|
endereco: ''
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Nome Completo *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.nome_completo}
|
||||||
|
onChange={(e) => setFormData({...formData, nome_completo: e.target.value})}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">E-mail</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({...formData, email: e.target.value})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">WhatsApp</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.whatsapp}
|
||||||
|
onChange={(e) => setFormData({...formData, whatsapp: e.target.value})}
|
||||||
|
placeholder="(11) 99999-9999"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Endereço</label>
|
||||||
|
<textarea
|
||||||
|
className="form-textarea"
|
||||||
|
value={formData.endereco}
|
||||||
|
onChange={(e) => setFormData({...formData, endereco: e.target.value})}
|
||||||
|
placeholder="Rua, número, bairro, cidade, CEP"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setEditingClient(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
{editingClient ? 'Atualizar' : 'Cadastrar'} Cliente
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Chat WhatsApp */}
|
||||||
|
<ChatWhatsApp
|
||||||
|
isOpen={showChatModal}
|
||||||
|
onClose={() => setShowChatModal(false)}
|
||||||
|
cliente={selectedCliente}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Clientes;
|
||||||
1720
client/src/pages/Configuracoes.js
Normal file
1720
client/src/pages/Configuracoes.js
Normal file
File diff suppressed because it is too large
Load Diff
33
client/src/pages/ConfiguracoesSimples.js
Normal file
33
client/src/pages/ConfiguracoesSimples.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FiSettings } from 'react-icons/fi';
|
||||||
|
|
||||||
|
const Configuracoes = () => {
|
||||||
|
return (
|
||||||
|
<div className="configuracoes fade-in">
|
||||||
|
<div className="page-header">
|
||||||
|
<div>
|
||||||
|
<h1>Configurações</h1>
|
||||||
|
<p>Configure as integrações e preferências do sistema</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="config-section">
|
||||||
|
<div className="config-header">
|
||||||
|
<div className="config-title">
|
||||||
|
<FiSettings className="config-icon" />
|
||||||
|
<div>
|
||||||
|
<h2>Evolution API - WhatsApp</h2>
|
||||||
|
<p>Configure a integração com Evolution API para envio de mensagens WhatsApp</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="config-placeholder">
|
||||||
|
<p>Configuração da Evolution API em desenvolvimento...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Configuracoes;
|
||||||
924
client/src/pages/Dashboard.js
Normal file
924
client/src/pages/Dashboard.js
Normal file
@@ -0,0 +1,924 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
FiPackage,
|
||||||
|
FiUsers,
|
||||||
|
FiTruck,
|
||||||
|
FiDollarSign,
|
||||||
|
FiShoppingCart,
|
||||||
|
FiTrendingUp,
|
||||||
|
FiTrendingDown,
|
||||||
|
FiBox,
|
||||||
|
FiX,
|
||||||
|
FiPlus,
|
||||||
|
FiMessageCircle,
|
||||||
|
FiCalendar,
|
||||||
|
FiClock,
|
||||||
|
FiAlertCircle
|
||||||
|
} from 'react-icons/fi';
|
||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
PieChart,
|
||||||
|
Pie,
|
||||||
|
Cell
|
||||||
|
} from 'recharts';
|
||||||
|
import { dashboardAPI, clientesAPI, despesasAPI, fornecedoresAPI } from '../services/api';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
const [dashboardData, setDashboardData] = useState({
|
||||||
|
contabilidade: {
|
||||||
|
receitaBruta: 0,
|
||||||
|
custosProdutos: 0,
|
||||||
|
totalDespesas: 0,
|
||||||
|
lucroReal: 0,
|
||||||
|
margemLucro: 0
|
||||||
|
},
|
||||||
|
resumoFinanceiro: {
|
||||||
|
receitasMes: 0,
|
||||||
|
despesasMes: 0,
|
||||||
|
lucroEstimado: 0,
|
||||||
|
totalVendas: 0
|
||||||
|
},
|
||||||
|
emprestimos: {
|
||||||
|
totalAberto: 0,
|
||||||
|
totalQuitado: 0,
|
||||||
|
quantidade: 0
|
||||||
|
},
|
||||||
|
vendasPrazo: {
|
||||||
|
total: 0,
|
||||||
|
quantidade: 0,
|
||||||
|
vendas: []
|
||||||
|
},
|
||||||
|
parcelasPendentes: {
|
||||||
|
quantidade: 0,
|
||||||
|
parcelas: []
|
||||||
|
},
|
||||||
|
estatisticas: {
|
||||||
|
totalProdutos: 0,
|
||||||
|
totalClientes: 0,
|
||||||
|
totalFornecedores: 0,
|
||||||
|
estoqueTotal: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Estados dos modais
|
||||||
|
const [showProdutoModal, setShowProdutoModal] = useState(false);
|
||||||
|
const [showClienteModal, setShowClienteModal] = useState(false);
|
||||||
|
const [showDespesaModal, setShowDespesaModal] = useState(false);
|
||||||
|
const [showVendaModal, setShowVendaModal] = useState(false);
|
||||||
|
|
||||||
|
// Estados dos formulários
|
||||||
|
const [fornecedores, setFornecedores] = useState([]);
|
||||||
|
const [tiposDespesas, setTiposDespesas] = useState([]);
|
||||||
|
const [clientes, setClientes] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
carregarDashboard();
|
||||||
|
carregarVendasPrazo();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const carregarDashboard = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const [dashboardRes, fornecedoresRes, tiposRes, clientesRes] = await Promise.all([
|
||||||
|
dashboardAPI.obterEstatisticas(),
|
||||||
|
fornecedoresAPI.listar(),
|
||||||
|
despesasAPI.listarTipos(),
|
||||||
|
clientesAPI.listar()
|
||||||
|
]);
|
||||||
|
|
||||||
|
setDashboardData(dashboardRes.data);
|
||||||
|
setFornecedores(fornecedoresRes.data);
|
||||||
|
setTiposDespesas(tiposRes.data);
|
||||||
|
setClientes(clientesRes.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar dashboard:', error);
|
||||||
|
toast.error('Erro ao carregar dados do dashboard');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const carregarVendasPrazo = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingVendasPrazo(true);
|
||||||
|
const response = await fetch('/api/vendas/prazo');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setVendasPrazo(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar vendas a prazo:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingVendasPrazo(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const enviarWhatsApp = async (venda) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/whatsapp/enviar-cobranca', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
vendaId: venda.id,
|
||||||
|
clienteId: venda.cliente_id,
|
||||||
|
telefone: venda.cliente_whatsapp || venda.cliente_telefone
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success('Mensagem enviada com sucesso!');
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
toast.error(error.message || 'Erro ao enviar mensagem');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao enviar WhatsApp:', error);
|
||||||
|
toast.error('Erro ao enviar mensagem');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatarDataVencimento = (data) => {
|
||||||
|
const dataVencimento = new Date(data);
|
||||||
|
const hoje = new Date();
|
||||||
|
const diffTime = dataVencimento - hoje;
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays < 0) {
|
||||||
|
return { texto: `${Math.abs(diffDays)} dias em atraso`, classe: 'vencido' };
|
||||||
|
} else if (diffDays === 0) {
|
||||||
|
return { texto: 'Vence hoje', classe: 'hoje' };
|
||||||
|
} else if (diffDays === 1) {
|
||||||
|
return { texto: 'Vence amanhã', classe: 'amanha' };
|
||||||
|
} else if (diffDays <= 7) {
|
||||||
|
return { texto: `${diffDays} dias`, classe: 'proximo' };
|
||||||
|
} else {
|
||||||
|
return { texto: `${diffDays} dias`, classe: 'futuro' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dados mockados para os gráficos (em um cenário real, viriam da API)
|
||||||
|
const vendasPorMes = [
|
||||||
|
{ mes: 'Jan', vendas: 12, valor: 2400 },
|
||||||
|
{ mes: 'Fev', vendas: 19, valor: 3800 },
|
||||||
|
{ mes: 'Mar', vendas: 15, valor: 3200 },
|
||||||
|
{ mes: 'Abr', vendas: 25, valor: 5100 },
|
||||||
|
{ mes: 'Mai', vendas: 22, valor: 4600 },
|
||||||
|
{ mes: 'Jun', vendas: 30, valor: 6200 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const produtosPorCategoria = [
|
||||||
|
{ name: 'Verão', value: 45, color: '#667eea' },
|
||||||
|
{ name: 'Inverno', value: 35, color: '#764ba2' },
|
||||||
|
{ name: 'Meia Estação', value: 20, color: '#f093fb' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const estatisticas = [
|
||||||
|
{
|
||||||
|
title: 'Total de Produtos',
|
||||||
|
value: stats.totalProdutos?.count || 0,
|
||||||
|
icon: FiPackage,
|
||||||
|
color: '#667eea',
|
||||||
|
bgColor: '#eef2ff',
|
||||||
|
trend: '+12%',
|
||||||
|
trendUp: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Clientes Cadastrados',
|
||||||
|
value: stats.totalClientes?.count || 0,
|
||||||
|
icon: FiUsers,
|
||||||
|
color: '#10b981',
|
||||||
|
bgColor: '#ecfdf5',
|
||||||
|
trend: '+8%',
|
||||||
|
trendUp: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Fornecedores',
|
||||||
|
value: stats.totalFornecedores?.count || 0,
|
||||||
|
icon: FiTruck,
|
||||||
|
color: '#f59e0b',
|
||||||
|
bgColor: '#fffbeb',
|
||||||
|
trend: '+2%',
|
||||||
|
trendUp: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Vendas do Mês',
|
||||||
|
value: stats.vendasMes?.count || 0,
|
||||||
|
icon: FiShoppingCart,
|
||||||
|
color: '#ef4444',
|
||||||
|
bgColor: '#fef2f2',
|
||||||
|
trend: '-5%',
|
||||||
|
trendUp: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Estoque Total',
|
||||||
|
value: stats.estoqueTotal?.total || 0,
|
||||||
|
icon: FiBox,
|
||||||
|
color: '#8b5cf6',
|
||||||
|
bgColor: '#f5f3ff',
|
||||||
|
trend: '+15%',
|
||||||
|
trendUp: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Faturamento Mensal',
|
||||||
|
value: `R$ ${(stats.vendasMes?.total || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`,
|
||||||
|
icon: FiDollarSign,
|
||||||
|
color: '#06b6d4',
|
||||||
|
bgColor: '#ecfeff',
|
||||||
|
trend: '+18%',
|
||||||
|
trendUp: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="loading">
|
||||||
|
<div>Carregando dashboard...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dashboard fade-in">
|
||||||
|
<div className="dashboard-header">
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p>Visão geral do seu negócio</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Resumo Financeiro e Ações Rápidas - MOVIDO PARA O TOPO */}
|
||||||
|
<div className="quick-summary">
|
||||||
|
<div className="summary-card">
|
||||||
|
<h3>Resumo Financeiro</h3>
|
||||||
|
<div className="summary-items">
|
||||||
|
<div className="summary-item">
|
||||||
|
<span className="summary-label">Receita do Mês:</span>
|
||||||
|
<span className="summary-value positive">
|
||||||
|
R$ {(stats.vendasMes?.total || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="summary-item">
|
||||||
|
<span className="summary-label">Despesas do Mês:</span>
|
||||||
|
<span className="summary-value negative">
|
||||||
|
R$ {(stats.despesasMes?.total || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="summary-item">
|
||||||
|
<span className="summary-label">Lucro Estimado:</span>
|
||||||
|
<span className="summary-value positive">
|
||||||
|
R$ {((stats.vendasMes?.total || 0) - (stats.despesasMes?.total || 0)).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vendas a Prazo e Recebimentos */}
|
||||||
|
<div className="summary-card vendas-prazo-card">
|
||||||
|
<div className="vendas-prazo-header">
|
||||||
|
<h3>
|
||||||
|
<FiCalendar />
|
||||||
|
Vendas a Prazo & Recebimentos
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-secondary"
|
||||||
|
onClick={carregarVendasPrazo}
|
||||||
|
disabled={loadingVendasPrazo}
|
||||||
|
>
|
||||||
|
{loadingVendasPrazo ? 'Carregando...' : 'Atualizar'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="vendas-prazo-content">
|
||||||
|
{loadingVendasPrazo ? (
|
||||||
|
<div className="loading-vendas">Carregando vendas...</div>
|
||||||
|
) : vendasPrazo.length === 0 ? (
|
||||||
|
<div className="empty-vendas">
|
||||||
|
<FiClock />
|
||||||
|
<p>Nenhuma venda a prazo encontrada</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="vendas-prazo-list">
|
||||||
|
{vendasPrazo.slice(0, 5).map((venda) => {
|
||||||
|
const vencimento = formatarDataVencimento(venda.data_vencimento);
|
||||||
|
return (
|
||||||
|
<div key={venda.id} className={`venda-prazo-item ${vencimento.classe}`}>
|
||||||
|
<div className="venda-info">
|
||||||
|
<div className="cliente-nome">{venda.cliente_nome}</div>
|
||||||
|
<div className="venda-detalhes">
|
||||||
|
<span className="valor">
|
||||||
|
R$ {parseFloat(venda.valor_parcela).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
|
</span>
|
||||||
|
<span className="parcela">
|
||||||
|
{venda.parcela_atual}/{venda.total_parcelas}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="venda-vencimento">
|
||||||
|
<span className={`vencimento-badge ${vencimento.classe}`}>
|
||||||
|
{vencimento.texto}
|
||||||
|
</span>
|
||||||
|
{(venda.cliente_whatsapp || venda.cliente_telefone) && (
|
||||||
|
<button
|
||||||
|
className="btn-whatsapp"
|
||||||
|
onClick={() => enviarWhatsApp(venda)}
|
||||||
|
title="Enviar lembrete via WhatsApp"
|
||||||
|
>
|
||||||
|
<FiMessageCircle />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{vendasPrazo.length > 5 && (
|
||||||
|
<div className="ver-mais">
|
||||||
|
<button className="btn btn-link">
|
||||||
|
Ver todas as {vendasPrazo.length} vendas a prazo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Gráficos */}
|
||||||
|
<div className="charts-grid">
|
||||||
|
{/* Gráfico de Vendas por Mês */}
|
||||||
|
<div className="chart-card">
|
||||||
|
<div className="chart-header">
|
||||||
|
<h3>Vendas por Mês</h3>
|
||||||
|
<p>Últimos 6 meses</p>
|
||||||
|
</div>
|
||||||
|
<div className="chart-container">
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<BarChart data={vendasPorMes}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="mes"
|
||||||
|
stroke="#6b7280"
|
||||||
|
fontSize={12}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
stroke="#6b7280"
|
||||||
|
fontSize={12}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="vendas"
|
||||||
|
fill="url(#colorGradient)"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="#667eea" stopOpacity={0.9}/>
|
||||||
|
<stop offset="95%" stopColor="#764ba2" stopOpacity={0.9}/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gráfico de Produtos por Categoria */}
|
||||||
|
<div className="chart-card">
|
||||||
|
<div className="chart-header">
|
||||||
|
<h3>Produtos por Estação</h3>
|
||||||
|
<p>Distribuição do estoque</p>
|
||||||
|
</div>
|
||||||
|
<div className="chart-container">
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={produtosPorCategoria}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={100}
|
||||||
|
paddingAngle={5}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{produtosPorCategoria.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
<div className="pie-legend">
|
||||||
|
{produtosPorCategoria.map((item, index) => (
|
||||||
|
<div key={index} className="legend-item">
|
||||||
|
<div
|
||||||
|
className="legend-color"
|
||||||
|
style={{ backgroundColor: item.color }}
|
||||||
|
></div>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
<span className="legend-value">{item.value}%</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modais */}
|
||||||
|
{showClienteModal && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>Novo Cliente</h2>
|
||||||
|
<button
|
||||||
|
className="btn-icon"
|
||||||
|
onClick={() => setShowClienteModal(false)}
|
||||||
|
>
|
||||||
|
<FiX />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-content">
|
||||||
|
<form onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
try {
|
||||||
|
await clientesAPI.criar({
|
||||||
|
nome_completo: formData.get('nome_completo'),
|
||||||
|
email: formData.get('email'),
|
||||||
|
telefone: formData.get('telefone'),
|
||||||
|
whatsapp: formData.get('whatsapp'),
|
||||||
|
endereco: formData.get('endereco')
|
||||||
|
});
|
||||||
|
toast.success('Cliente criado com sucesso!');
|
||||||
|
setShowClienteModal(false);
|
||||||
|
carregarDashboard();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Erro ao criar cliente');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Nome Completo *</label>
|
||||||
|
<input type="text" name="nome_completo" className="form-input" required />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Email</label>
|
||||||
|
<input type="email" name="email" className="form-input" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Telefone</label>
|
||||||
|
<input type="text" name="telefone" className="form-input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">WhatsApp</label>
|
||||||
|
<input type="text" name="whatsapp" className="form-input" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Endereço</label>
|
||||||
|
<input type="text" name="endereco" className="form-input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => setShowClienteModal(false)}>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
<FiPlus />
|
||||||
|
Criar Cliente
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDespesaModal && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>Nova Despesa</h2>
|
||||||
|
<button
|
||||||
|
className="btn-icon"
|
||||||
|
onClick={() => setShowDespesaModal(false)}
|
||||||
|
>
|
||||||
|
<FiX />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-content">
|
||||||
|
<form onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
try {
|
||||||
|
await despesasAPI.criar({
|
||||||
|
tipo_despesa_id: formData.get('tipo_despesa_id'),
|
||||||
|
fornecedor_id: formData.get('fornecedor_id') || null,
|
||||||
|
data_despesa: formData.get('data_despesa'),
|
||||||
|
valor: parseFloat(formData.get('valor')),
|
||||||
|
descricao: formData.get('descricao')
|
||||||
|
});
|
||||||
|
toast.success('Despesa criada com sucesso!');
|
||||||
|
setShowDespesaModal(false);
|
||||||
|
carregarDashboard();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Erro ao criar despesa');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<div className="grid grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Tipo de Despesa *</label>
|
||||||
|
<select name="tipo_despesa_id" className="form-select" required>
|
||||||
|
<option value="">Selecione o tipo</option>
|
||||||
|
{tiposDespesas.map(tipo => (
|
||||||
|
<option key={tipo.id} value={tipo.id}>{tipo.nome}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Fornecedor</label>
|
||||||
|
<select name="fornecedor_id" className="form-select">
|
||||||
|
<option value="">Selecione o fornecedor</option>
|
||||||
|
{fornecedores.map(fornecedor => (
|
||||||
|
<option key={fornecedor.id} value={fornecedor.id}>{fornecedor.razao_social}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Data da Despesa *</label>
|
||||||
|
<input type="date" name="data_despesa" className="form-input" required />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Valor *</label>
|
||||||
|
<input type="number" step="0.01" name="valor" className="form-input" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Descrição</label>
|
||||||
|
<textarea name="descricao" className="form-textarea" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => setShowDespesaModal(false)}>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
<FiPlus />
|
||||||
|
Criar Despesa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal Novo Produto */}
|
||||||
|
{showProdutoModal && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>Novo Produto</h2>
|
||||||
|
<button
|
||||||
|
className="btn-icon"
|
||||||
|
onClick={() => setShowProdutoModal(false)}
|
||||||
|
>
|
||||||
|
<FiX />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-content">
|
||||||
|
<form onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
|
||||||
|
// Criar FormData para envio
|
||||||
|
const produtoFormData = new FormData();
|
||||||
|
produtoFormData.append('id_produto', formData.get('id_produto') || '');
|
||||||
|
produtoFormData.append('marca', formData.get('marca'));
|
||||||
|
produtoFormData.append('nome', formData.get('nome'));
|
||||||
|
produtoFormData.append('estacao', formData.get('estacao'));
|
||||||
|
produtoFormData.append('genero', formData.get('genero'));
|
||||||
|
produtoFormData.append('fornecedor_id', formData.get('fornecedor_id') || '');
|
||||||
|
produtoFormData.append('valor_compra', formData.get('valor_compra'));
|
||||||
|
produtoFormData.append('valor_revenda', formData.get('valor_revenda'));
|
||||||
|
|
||||||
|
// Adicionar variação básica
|
||||||
|
const variacaoBasica = [{
|
||||||
|
tamanho: formData.get('tamanho') || 'Único',
|
||||||
|
cor: formData.get('cor') || 'Padrão',
|
||||||
|
quantidade: parseInt(formData.get('quantidade')) || 1
|
||||||
|
}];
|
||||||
|
produtoFormData.append('variacoes_data', JSON.stringify(variacaoBasica));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/produtos', {
|
||||||
|
method: 'POST',
|
||||||
|
body: produtoFormData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success('Produto criado com sucesso!');
|
||||||
|
setShowProdutoModal(false);
|
||||||
|
carregarDashboard();
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
toast.error(error.error || 'Erro ao criar produto');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar produto:', error);
|
||||||
|
toast.error('Erro ao criar produto');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<div className="grid grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">ID do Produto</label>
|
||||||
|
<input type="text" name="id_produto" className="form-input" placeholder="Ex: LK001" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Marca *</label>
|
||||||
|
<input type="text" name="marca" className="form-input" required placeholder="Ex: Nike" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Nome do Produto *</label>
|
||||||
|
<input type="text" name="nome" className="form-input" required placeholder="Ex: Camiseta Infantil" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-3">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Estação *</label>
|
||||||
|
<select name="estacao" className="form-select" required>
|
||||||
|
<option value="Verão">Verão</option>
|
||||||
|
<option value="Inverno">Inverno</option>
|
||||||
|
<option value="Meia Estação">Meia Estação</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Gênero *</label>
|
||||||
|
<select name="genero" className="form-select" required>
|
||||||
|
<option value="Menino">Menino</option>
|
||||||
|
<option value="Menina">Menina</option>
|
||||||
|
<option value="Unissex">Unissex</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Fornecedor</label>
|
||||||
|
<select name="fornecedor_id" className="form-select">
|
||||||
|
<option value="">Selecione</option>
|
||||||
|
{fornecedores.map(fornecedor => (
|
||||||
|
<option key={fornecedor.id} value={fornecedor.id}>{fornecedor.razao_social}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Valor de Compra *</label>
|
||||||
|
<input type="number" step="0.01" name="valor_compra" className="form-input" required placeholder="0,00" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Valor de Revenda *</label>
|
||||||
|
<input type="number" step="0.01" name="valor_revenda" className="form-input" required placeholder="0,00" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-section">
|
||||||
|
<h4>Variação Básica</h4>
|
||||||
|
<div className="grid grid-3">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Tamanho</label>
|
||||||
|
<input type="text" name="tamanho" className="form-input" placeholder="Ex: M, 6, 8" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Cor</label>
|
||||||
|
<input type="text" name="cor" className="form-input" placeholder="Ex: Azul, Rosa" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Quantidade</label>
|
||||||
|
<input type="number" name="quantidade" className="form-input" placeholder="1" min="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => setShowProdutoModal(false)}>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
<FiPlus />
|
||||||
|
Criar Produto
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal Nova Venda */}
|
||||||
|
{showVendaModal && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>Nova Venda</h2>
|
||||||
|
<button
|
||||||
|
className="btn-icon"
|
||||||
|
onClick={() => setShowVendaModal(false)}
|
||||||
|
>
|
||||||
|
<FiX />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-content">
|
||||||
|
<form onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
|
||||||
|
const vendaData = {
|
||||||
|
cliente_id: formData.get('cliente_id') || null,
|
||||||
|
tipo_pagamento: formData.get('tipo_pagamento'),
|
||||||
|
valor_total: parseFloat(formData.get('valor_total')),
|
||||||
|
desconto: parseFloat(formData.get('desconto')) || 0,
|
||||||
|
parcelas: parseInt(formData.get('parcelas')) || 1,
|
||||||
|
valor_parcela: parseFloat(formData.get('valor_parcela')) || 0,
|
||||||
|
data_venda: formData.get('data_venda'),
|
||||||
|
observacoes: formData.get('observacoes') || '',
|
||||||
|
itens: [] // Venda simples sem itens específicos
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/vendas', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(vendaData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success('Venda registrada com sucesso!');
|
||||||
|
setShowVendaModal(false);
|
||||||
|
carregarDashboard();
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
toast.error(error.error || 'Erro ao registrar venda');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao registrar venda:', error);
|
||||||
|
toast.error('Erro ao registrar venda');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<div className="grid grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Cliente</label>
|
||||||
|
<select name="cliente_id" className="form-select">
|
||||||
|
<option value="">Venda sem cliente cadastrado</option>
|
||||||
|
{clientes.map(cliente => (
|
||||||
|
<option key={cliente.id} value={cliente.id}>{cliente.nome_completo}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Data da Venda *</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="data_venda"
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
defaultValue={new Date().toISOString().split('T')[0]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Tipo de Pagamento *</label>
|
||||||
|
<select name="tipo_pagamento" className="form-select" required onChange={(e) => {
|
||||||
|
const parcelasField = document.querySelector('input[name="parcelas"]');
|
||||||
|
const valorParcelaField = document.querySelector('input[name="valor_parcela"]');
|
||||||
|
if (e.target.value === 'vista') {
|
||||||
|
parcelasField.value = '1';
|
||||||
|
parcelasField.disabled = true;
|
||||||
|
valorParcelaField.disabled = true;
|
||||||
|
} else {
|
||||||
|
parcelasField.disabled = false;
|
||||||
|
valorParcelaField.disabled = false;
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<option value="">Selecione</option>
|
||||||
|
<option value="vista">À Vista</option>
|
||||||
|
<option value="parcelado">Parcelado</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Valor Total *</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
name="valor_total"
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
placeholder="0,00"
|
||||||
|
onChange={(e) => {
|
||||||
|
const parcelas = parseInt(document.querySelector('input[name="parcelas"]').value) || 1;
|
||||||
|
const desconto = parseFloat(document.querySelector('input[name="desconto"]').value) || 0;
|
||||||
|
const valorTotal = parseFloat(e.target.value) || 0;
|
||||||
|
const valorFinal = valorTotal - desconto;
|
||||||
|
const valorParcela = valorFinal / parcelas;
|
||||||
|
document.querySelector('input[name="valor_parcela"]').value = valorParcela.toFixed(2);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-3">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Desconto</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
name="desconto"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="0,00"
|
||||||
|
onChange={(e) => {
|
||||||
|
const parcelas = parseInt(document.querySelector('input[name="parcelas"]').value) || 1;
|
||||||
|
const valorTotal = parseFloat(document.querySelector('input[name="valor_total"]').value) || 0;
|
||||||
|
const desconto = parseFloat(e.target.value) || 0;
|
||||||
|
const valorFinal = valorTotal - desconto;
|
||||||
|
const valorParcela = valorFinal / parcelas;
|
||||||
|
document.querySelector('input[name="valor_parcela"]').value = valorParcela.toFixed(2);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Parcelas</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="parcelas"
|
||||||
|
className="form-input"
|
||||||
|
min="1"
|
||||||
|
defaultValue="1"
|
||||||
|
onChange={(e) => {
|
||||||
|
const valorTotal = parseFloat(document.querySelector('input[name="valor_total"]').value) || 0;
|
||||||
|
const desconto = parseFloat(document.querySelector('input[name="desconto"]').value) || 0;
|
||||||
|
const parcelas = parseInt(e.target.value) || 1;
|
||||||
|
const valorFinal = valorTotal - desconto;
|
||||||
|
const valorParcela = valorFinal / parcelas;
|
||||||
|
document.querySelector('input[name="valor_parcela"]').value = valorParcela.toFixed(2);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Valor por Parcela</label>
|
||||||
|
<input type="number" step="0.01" name="valor_parcela" className="form-input" readOnly />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Observações</label>
|
||||||
|
<textarea name="observacoes" className="form-textarea" rows="3" placeholder="Observações sobre a venda..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => setShowVendaModal(false)}>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
<FiPlus />
|
||||||
|
Registrar Venda
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
333
client/src/pages/DashboardNovo.js
Normal file
333
client/src/pages/DashboardNovo.js
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
FiDollarSign,
|
||||||
|
FiTrendingUp,
|
||||||
|
FiTrendingDown,
|
||||||
|
FiClock,
|
||||||
|
FiAlertCircle,
|
||||||
|
FiPackage,
|
||||||
|
FiUsers,
|
||||||
|
FiTruck,
|
||||||
|
FiCreditCard,
|
||||||
|
FiCalendar,
|
||||||
|
FiMessageSquare
|
||||||
|
} from 'react-icons/fi';
|
||||||
|
import { dashboardAPI } from '../services/api';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import '../styles/dashboard-contabilidade.css';
|
||||||
|
|
||||||
|
const DashboardNovo = () => {
|
||||||
|
const [dashboardData, setDashboardData] = useState({
|
||||||
|
contabilidade: {
|
||||||
|
receitaBruta: 0,
|
||||||
|
custosProdutos: 0,
|
||||||
|
totalDespesas: 0,
|
||||||
|
lucroReal: 0,
|
||||||
|
margemLucro: 0
|
||||||
|
},
|
||||||
|
resumoFinanceiro: {
|
||||||
|
receitasMes: 0,
|
||||||
|
despesasMes: 0,
|
||||||
|
lucroEstimado: 0,
|
||||||
|
totalVendas: 0
|
||||||
|
},
|
||||||
|
emprestimos: {
|
||||||
|
totalAberto: 0,
|
||||||
|
totalQuitado: 0,
|
||||||
|
quantidade: 0
|
||||||
|
},
|
||||||
|
vendasPrazo: {
|
||||||
|
total: 0,
|
||||||
|
quantidade: 0,
|
||||||
|
vendas: []
|
||||||
|
},
|
||||||
|
parcelasPendentes: {
|
||||||
|
quantidade: 0,
|
||||||
|
parcelas: []
|
||||||
|
},
|
||||||
|
estatisticas: {
|
||||||
|
totalProdutos: 0,
|
||||||
|
totalClientes: 0,
|
||||||
|
totalFornecedores: 0,
|
||||||
|
estoqueTotal: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
carregarDashboard();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const carregarDashboard = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await dashboardAPI.obterEstatisticas();
|
||||||
|
setDashboardData(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar dashboard:', error);
|
||||||
|
toast.error('Erro ao carregar dados do dashboard');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatarMoeda = (valor) => {
|
||||||
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BRL'
|
||||||
|
}).format(valor);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatarData = (data) => {
|
||||||
|
return new Date(data).toLocaleDateString('pt-BR');
|
||||||
|
};
|
||||||
|
|
||||||
|
const enviarWhatsApp = (venda) => {
|
||||||
|
const telefone = venda.cliente_whatsapp || venda.cliente_telefone;
|
||||||
|
if (!telefone) {
|
||||||
|
toast.error('Cliente não possui WhatsApp cadastrado');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numeroLimpo = telefone.replace(/\D/g, '');
|
||||||
|
const mensagem = `Olá! Sua compra no valor de ${formatarMoeda(venda.valor)} vence em ${formatarData(venda.vencimento)}. Obrigado!`;
|
||||||
|
const mensagemCodificada = encodeURIComponent(mensagem);
|
||||||
|
const url = `https://wa.me/55${numeroLimpo}?text=${mensagemCodificada}`;
|
||||||
|
|
||||||
|
window.open(url, '_blank');
|
||||||
|
toast.success('WhatsApp aberto com sucesso!');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="dashboard-loading">
|
||||||
|
<div className="loading-spinner"></div>
|
||||||
|
<p>Carregando dashboard...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dashboard">
|
||||||
|
<div className="dashboard-header">
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p>Visão geral do seu negócio</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CONTABILIDADE COMPLETA */}
|
||||||
|
<div className="contabilidade-section">
|
||||||
|
<h2>📊 Contabilidade Completa - {new Date().toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' })}</h2>
|
||||||
|
<div className="contabilidade-grid">
|
||||||
|
<div className="contabilidade-card receita">
|
||||||
|
<div className="card-header">
|
||||||
|
<FiTrendingUp className="card-icon" />
|
||||||
|
<span>Receita Bruta</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-value">{formatarMoeda(dashboardData.contabilidade.receitaBruta)}</div>
|
||||||
|
<div className="card-subtitle">Vendas do mês</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="contabilidade-card custo">
|
||||||
|
<div className="card-header">
|
||||||
|
<FiPackage className="card-icon" />
|
||||||
|
<span>Custos dos Produtos</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-value">{formatarMoeda(dashboardData.contabilidade.custosProdutos)}</div>
|
||||||
|
<div className="card-subtitle">Custo das mercadorias vendidas</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="contabilidade-card despesa">
|
||||||
|
<div className="card-header">
|
||||||
|
<FiTrendingDown className="card-icon" />
|
||||||
|
<span>Despesas</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-value">{formatarMoeda(dashboardData.contabilidade.totalDespesas)}</div>
|
||||||
|
<div className="card-subtitle">Gastos operacionais</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="contabilidade-card lucro">
|
||||||
|
<div className="card-header">
|
||||||
|
<FiDollarSign className="card-icon" />
|
||||||
|
<span>Lucro Real</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-value">{formatarMoeda(dashboardData.contabilidade.lucroReal)}</div>
|
||||||
|
<div className="card-subtitle">
|
||||||
|
Margem: {dashboardData.contabilidade.margemLucro}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RESUMO FINANCEIRO */}
|
||||||
|
<div className="resumo-section">
|
||||||
|
<h2>💰 Resumo Financeiro</h2>
|
||||||
|
<div className="resumo-grid">
|
||||||
|
<div className="resumo-card">
|
||||||
|
<div className="resumo-header">
|
||||||
|
<span>Receitas do Mês</span>
|
||||||
|
<FiTrendingUp className="resumo-icon success" />
|
||||||
|
</div>
|
||||||
|
<div className="resumo-value success">
|
||||||
|
{formatarMoeda(dashboardData.resumoFinanceiro.receitasMes)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="resumo-card">
|
||||||
|
<div className="resumo-header">
|
||||||
|
<span>Despesas do Mês</span>
|
||||||
|
<FiTrendingDown className="resumo-icon danger" />
|
||||||
|
</div>
|
||||||
|
<div className="resumo-value danger">
|
||||||
|
{formatarMoeda(dashboardData.resumoFinanceiro.despesasMes)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="resumo-card">
|
||||||
|
<div className="resumo-header">
|
||||||
|
<span>Lucro Estimado</span>
|
||||||
|
<FiDollarSign className="resumo-icon primary" />
|
||||||
|
</div>
|
||||||
|
<div className="resumo-value primary">
|
||||||
|
{formatarMoeda(dashboardData.resumoFinanceiro.lucroEstimado)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* VENDAS A PRAZO & RECEBIMENTOS */}
|
||||||
|
<div className="vendas-prazo-section">
|
||||||
|
<h2>📅 Vendas a Prazo & Recebimentos</h2>
|
||||||
|
|
||||||
|
{dashboardData.vendasPrazo.quantidade > 0 ? (
|
||||||
|
<div className="vendas-prazo-container">
|
||||||
|
<div className="vendas-prazo-header">
|
||||||
|
<div className="prazo-summary">
|
||||||
|
<span className="prazo-count">{dashboardData.vendasPrazo.quantidade} vendas</span>
|
||||||
|
<span className="prazo-total">{formatarMoeda(dashboardData.vendasPrazo.total)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="vendas-prazo-list">
|
||||||
|
{dashboardData.vendasPrazo.vendas.map((venda, index) => (
|
||||||
|
<div key={index} className="venda-prazo-item">
|
||||||
|
<div className="venda-info">
|
||||||
|
<div className="cliente-nome">{venda.cliente}</div>
|
||||||
|
<div className="venda-valor">{formatarMoeda(venda.valor)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="venda-vencimento">
|
||||||
|
<FiCalendar className="vencimento-icon" />
|
||||||
|
<span>Vence em: {formatarData(venda.vencimento)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="venda-actions">
|
||||||
|
<button
|
||||||
|
className="btn-whatsapp"
|
||||||
|
onClick={() => enviarWhatsApp(venda)}
|
||||||
|
title="Enviar cobrança via WhatsApp"
|
||||||
|
>
|
||||||
|
<FiMessageSquare />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="empty-state">
|
||||||
|
<FiClock className="empty-icon" />
|
||||||
|
<p>Nenhuma venda a prazo encontrada</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* PARCELAS PENDENTES */}
|
||||||
|
{dashboardData.parcelasPendentes.quantidade > 0 && (
|
||||||
|
<div className="parcelas-section">
|
||||||
|
<h2>💳 Parcelas Pendentes</h2>
|
||||||
|
<div className="parcelas-list">
|
||||||
|
{dashboardData.parcelasPendentes.parcelas.map((parcela, index) => (
|
||||||
|
<div key={index} className="parcela-item">
|
||||||
|
<div className="parcela-info">
|
||||||
|
<div className="parcela-cliente">{parcela.cliente}</div>
|
||||||
|
<div className="parcela-valor">{formatarMoeda(parcela.valor)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="parcela-detalhes">
|
||||||
|
<span>{parcela.parcelas}x parcelas</span>
|
||||||
|
<span>Próximo: {formatarData(parcela.proximoVencimento)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* EMPRÉSTIMOS */}
|
||||||
|
<div className="emprestimos-section">
|
||||||
|
<h2>🏦 Empréstimos</h2>
|
||||||
|
<div className="emprestimos-grid">
|
||||||
|
<div className="emprestimo-card aberto">
|
||||||
|
<div className="emprestimo-header">
|
||||||
|
<FiAlertCircle className="emprestimo-icon" />
|
||||||
|
<span>Em Aberto</span>
|
||||||
|
</div>
|
||||||
|
<div className="emprestimo-value">
|
||||||
|
{formatarMoeda(dashboardData.emprestimos.totalAberto)}
|
||||||
|
</div>
|
||||||
|
<div className="emprestimo-count">
|
||||||
|
{dashboardData.emprestimos.quantidade} empréstimos
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="emprestimo-card quitado">
|
||||||
|
<div className="emprestimo-header">
|
||||||
|
<FiTrendingUp className="emprestimo-icon" />
|
||||||
|
<span>Quitados</span>
|
||||||
|
</div>
|
||||||
|
<div className="emprestimo-value">
|
||||||
|
{formatarMoeda(dashboardData.emprestimos.totalQuitado)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ESTATÍSTICAS GERAIS */}
|
||||||
|
<div className="estatisticas-section">
|
||||||
|
<h2>📈 Estatísticas Gerais</h2>
|
||||||
|
<div className="estatisticas-grid">
|
||||||
|
<div className="stat-card">
|
||||||
|
<FiPackage className="stat-icon" />
|
||||||
|
<div className="stat-info">
|
||||||
|
<div className="stat-value">{dashboardData.estatisticas.totalProdutos}</div>
|
||||||
|
<div className="stat-label">Produtos</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat-card">
|
||||||
|
<FiUsers className="stat-icon" />
|
||||||
|
<div className="stat-info">
|
||||||
|
<div className="stat-value">{dashboardData.estatisticas.totalClientes}</div>
|
||||||
|
<div className="stat-label">Clientes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat-card">
|
||||||
|
<FiTruck className="stat-icon" />
|
||||||
|
<div className="stat-info">
|
||||||
|
<div className="stat-value">{dashboardData.estatisticas.totalFornecedores}</div>
|
||||||
|
<div className="stat-label">Fornecedores</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat-card">
|
||||||
|
<FiPackage className="stat-icon" />
|
||||||
|
<div className="stat-info">
|
||||||
|
<div className="stat-value">{dashboardData.estatisticas.estoqueTotal}</div>
|
||||||
|
<div className="stat-label">Estoque Total</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardNovo;
|
||||||
246
client/src/pages/DashboardSimples.js
Normal file
246
client/src/pages/DashboardSimples.js
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
FiDollarSign,
|
||||||
|
FiTrendingUp,
|
||||||
|
FiTrendingDown,
|
||||||
|
FiClock,
|
||||||
|
FiPackage,
|
||||||
|
FiUsers,
|
||||||
|
FiCalendar,
|
||||||
|
FiMessageSquare
|
||||||
|
} from 'react-icons/fi';
|
||||||
|
import { dashboardAPI } from '../services/api';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import '../styles/dashboard-simples.css';
|
||||||
|
|
||||||
|
const DashboardSimples = () => {
|
||||||
|
const [dashboardData, setDashboardData] = useState({
|
||||||
|
contabilidade: {
|
||||||
|
receitaBruta: 0,
|
||||||
|
custosProdutos: 0,
|
||||||
|
totalDespesas: 0,
|
||||||
|
lucroReal: 0,
|
||||||
|
margemLucro: 0
|
||||||
|
},
|
||||||
|
vendasPrazo: {
|
||||||
|
total: 0,
|
||||||
|
quantidade: 0,
|
||||||
|
vendas: []
|
||||||
|
},
|
||||||
|
parcelasPendentes: {
|
||||||
|
quantidade: 0,
|
||||||
|
parcelas: []
|
||||||
|
},
|
||||||
|
estatisticas: {
|
||||||
|
totalProdutos: 0,
|
||||||
|
totalClientes: 0,
|
||||||
|
totalFornecedores: 0,
|
||||||
|
estoqueTotal: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
carregarDashboard();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const carregarDashboard = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await dashboardAPI.obterEstatisticas();
|
||||||
|
setDashboardData(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar dashboard:', error);
|
||||||
|
toast.error('Erro ao carregar dados do dashboard');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatarMoeda = (valor) => {
|
||||||
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BRL'
|
||||||
|
}).format(valor);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatarData = (data) => {
|
||||||
|
return new Date(data).toLocaleDateString('pt-BR');
|
||||||
|
};
|
||||||
|
|
||||||
|
const enviarWhatsApp = (venda) => {
|
||||||
|
const telefone = venda.cliente_whatsapp || venda.cliente_telefone;
|
||||||
|
if (!telefone) {
|
||||||
|
toast.error('Cliente não possui WhatsApp cadastrado');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numeroLimpo = telefone.replace(/\D/g, '');
|
||||||
|
const mensagem = `Olá! Sua compra no valor de ${formatarMoeda(venda.valor)} vence em ${formatarData(venda.vencimento)}. Obrigado!`;
|
||||||
|
const mensagemCodificada = encodeURIComponent(mensagem);
|
||||||
|
const url = `https://wa.me/55${numeroLimpo}?text=${mensagemCodificada}`;
|
||||||
|
|
||||||
|
window.open(url, '_blank');
|
||||||
|
toast.success('WhatsApp aberto com sucesso!');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="dashboard-loading">
|
||||||
|
<div className="loading-spinner"></div>
|
||||||
|
<p>Carregando dashboard...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mesAtual = new Date().toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dashboard-simples">
|
||||||
|
<div className="dashboard-header">
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p>Visão geral do seu negócio - {mesAtual}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RESUMO FINANCEIRO PRINCIPAL */}
|
||||||
|
<div className="resumo-principal">
|
||||||
|
<div className="card-principal receitas">
|
||||||
|
<div className="card-icon">
|
||||||
|
<FiTrendingUp />
|
||||||
|
</div>
|
||||||
|
<div className="card-content">
|
||||||
|
<h3>Receitas do Mês</h3>
|
||||||
|
<div className="valor-principal">{formatarMoeda(dashboardData.contabilidade.receitaBruta)}</div>
|
||||||
|
<div className="subtexto">Vendas realizadas</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card-principal despesas">
|
||||||
|
<div className="card-icon">
|
||||||
|
<FiTrendingDown />
|
||||||
|
</div>
|
||||||
|
<div className="card-content">
|
||||||
|
<h3>Despesas do Mês</h3>
|
||||||
|
<div className="valor-principal">{formatarMoeda(dashboardData.contabilidade.totalDespesas)}</div>
|
||||||
|
<div className="subtexto">Gastos operacionais</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card-principal lucro">
|
||||||
|
<div className="card-icon">
|
||||||
|
<FiDollarSign />
|
||||||
|
</div>
|
||||||
|
<div className="card-content">
|
||||||
|
<h3>Lucro Real</h3>
|
||||||
|
<div className="valor-principal">{formatarMoeda(dashboardData.contabilidade.lucroReal)}</div>
|
||||||
|
<div className="subtexto">Margem: {dashboardData.contabilidade.margemLucro}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* VENDAS A PRAZO */}
|
||||||
|
{dashboardData.vendasPrazo.quantidade > 0 && (
|
||||||
|
<div className="secao-vendas-prazo">
|
||||||
|
<div className="secao-header">
|
||||||
|
<h2>📅 Vendas a Prazo & Recebimentos</h2>
|
||||||
|
<div className="total-prazo">
|
||||||
|
Total: {formatarMoeda(dashboardData.vendasPrazo.total)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lista-vendas-prazo">
|
||||||
|
{dashboardData.vendasPrazo.vendas.slice(0, 5).map((venda, index) => (
|
||||||
|
<div key={index} className="item-venda-prazo">
|
||||||
|
<div className="venda-info">
|
||||||
|
<div className="cliente">{venda.cliente}</div>
|
||||||
|
<div className="valor">{formatarMoeda(venda.valor)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="venda-vencimento">
|
||||||
|
<FiCalendar />
|
||||||
|
<span>{formatarData(venda.vencimento)}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn-whatsapp-simples"
|
||||||
|
onClick={() => enviarWhatsApp(venda)}
|
||||||
|
title="Enviar WhatsApp"
|
||||||
|
>
|
||||||
|
<FiMessageSquare />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{dashboardData.vendasPrazo.quantidade > 5 && (
|
||||||
|
<div className="mais-vendas">
|
||||||
|
+{dashboardData.vendasPrazo.quantidade - 5} vendas a prazo
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PARCELAS PENDENTES */}
|
||||||
|
{dashboardData.parcelasPendentes.quantidade > 0 && (
|
||||||
|
<div className="secao-parcelas">
|
||||||
|
<div className="secao-header">
|
||||||
|
<h2>💳 Parcelas Pendentes</h2>
|
||||||
|
<div className="total-parcelas">
|
||||||
|
{dashboardData.parcelasPendentes.quantidade} parcelas
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lista-parcelas">
|
||||||
|
{dashboardData.parcelasPendentes.parcelas.slice(0, 3).map((parcela, index) => (
|
||||||
|
<div key={index} className="item-parcela">
|
||||||
|
<div className="parcela-info">
|
||||||
|
<div className="cliente">{parcela.cliente}</div>
|
||||||
|
<div className="valor">{formatarMoeda(parcela.valor)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="parcela-detalhes">
|
||||||
|
<span>{parcela.parcelas}x parcelas</span>
|
||||||
|
<span>Próximo: {formatarData(parcela.proximoVencimento)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ESTATÍSTICAS RÁPIDAS */}
|
||||||
|
<div className="estatisticas-rapidas">
|
||||||
|
<div className="stat-item">
|
||||||
|
<FiPackage className="stat-icon" />
|
||||||
|
<div className="stat-info">
|
||||||
|
<div className="stat-numero">{dashboardData.estatisticas.totalProdutos}</div>
|
||||||
|
<div className="stat-label">Produtos</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat-item">
|
||||||
|
<FiUsers className="stat-icon" />
|
||||||
|
<div className="stat-info">
|
||||||
|
<div className="stat-numero">{dashboardData.estatisticas.totalClientes}</div>
|
||||||
|
<div className="stat-label">Clientes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat-item">
|
||||||
|
<FiPackage className="stat-icon" />
|
||||||
|
<div className="stat-info">
|
||||||
|
<div className="stat-numero">{dashboardData.estatisticas.estoqueTotal}</div>
|
||||||
|
<div className="stat-label">Estoque</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* EMPTY STATES */}
|
||||||
|
{dashboardData.vendasPrazo.quantidade === 0 && dashboardData.parcelasPendentes.quantidade === 0 && (
|
||||||
|
<div className="empty-recebimentos">
|
||||||
|
<FiClock className="empty-icon" />
|
||||||
|
<h3>Nenhum recebimento pendente</h3>
|
||||||
|
<p>Todas as vendas foram pagas à vista</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardSimples;
|
||||||
447
client/src/pages/Despesas.js
Normal file
447
client/src/pages/Despesas.js
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
FiPlus,
|
||||||
|
FiEdit,
|
||||||
|
FiTrash2,
|
||||||
|
FiSearch,
|
||||||
|
FiDollarSign,
|
||||||
|
FiCalendar,
|
||||||
|
FiTag
|
||||||
|
} from 'react-icons/fi';
|
||||||
|
import { despesasAPI, fornecedoresAPI } from '../services/api';
|
||||||
|
import ViewToggle from '../components/ViewToggle';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
const Despesas = () => {
|
||||||
|
const [despesas, setDespesas] = useState([]);
|
||||||
|
const [tiposDespesas, setTiposDespesas] = useState([]);
|
||||||
|
const [fornecedores, setFornecedores] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [viewMode, setViewMode] = useState('list');
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [showTipoModal, setShowTipoModal] = useState(false);
|
||||||
|
const [editingExpense, setEditingExpense] = useState(null);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
tipo_despesa: '',
|
||||||
|
fornecedor: '',
|
||||||
|
data: new Date().toISOString().split('T')[0],
|
||||||
|
valor: '',
|
||||||
|
descricao: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const [novoTipo, setNovoTipo] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
carregarDados();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const carregarDados = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const [despesasRes, tiposRes, fornecedoresRes] = await Promise.all([
|
||||||
|
despesasAPI.listar(),
|
||||||
|
despesasAPI.listarTipos(),
|
||||||
|
fornecedoresAPI.listar()
|
||||||
|
]);
|
||||||
|
setDespesas(despesasRes.data);
|
||||||
|
setTiposDespesas(tiposRes.data);
|
||||||
|
setFornecedores(fornecedoresRes.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar dados:', error);
|
||||||
|
toast.error('Erro ao carregar despesas');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
if (editingExpense) {
|
||||||
|
await despesasAPI.atualizar(editingExpense.id, formData);
|
||||||
|
toast.success('Despesa atualizada com sucesso!');
|
||||||
|
} else {
|
||||||
|
await despesasAPI.criar(formData);
|
||||||
|
toast.success('Despesa cadastrada com sucesso!');
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowModal(false);
|
||||||
|
setEditingExpense(null);
|
||||||
|
setFormData({
|
||||||
|
tipo_despesa: '',
|
||||||
|
fornecedor: '',
|
||||||
|
data: new Date().toISOString().split('T')[0],
|
||||||
|
valor: '',
|
||||||
|
descricao: ''
|
||||||
|
});
|
||||||
|
carregarDados();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao salvar despesa:', error);
|
||||||
|
toast.error('Erro ao salvar despesa');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddTipo = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
await despesasAPI.criarTipo({ nome: novoTipo });
|
||||||
|
toast.success('Tipo de despesa criado com sucesso!');
|
||||||
|
setShowTipoModal(false);
|
||||||
|
setNovoTipo('');
|
||||||
|
carregarDados();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar tipo de despesa:', error);
|
||||||
|
toast.error('Erro ao criar tipo de despesa');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (despesa) => {
|
||||||
|
setEditingExpense(despesa);
|
||||||
|
setFormData({
|
||||||
|
tipo_despesa: despesa.tipo_nome || '',
|
||||||
|
fornecedor: despesa.fornecedor_nome || '',
|
||||||
|
data: despesa.data,
|
||||||
|
valor: despesa.valor,
|
||||||
|
descricao: despesa.descricao || ''
|
||||||
|
});
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (window.confirm('Tem certeza que deseja excluir esta despesa?')) {
|
||||||
|
try {
|
||||||
|
await despesasAPI.deletar(id);
|
||||||
|
toast.success('Despesa excluída com sucesso!');
|
||||||
|
carregarDados();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao excluir despesa:', error);
|
||||||
|
toast.error('Erro ao excluir despesa');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredDespesas = despesas.filter(despesa =>
|
||||||
|
(despesa.descricao && despesa.descricao.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||||
|
(despesa.tipo_nome && despesa.tipo_nome.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||||
|
(despesa.fornecedor_nome && despesa.fornecedor_nome.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalDespesas = filteredDespesas.reduce((total, despesa) => total + parseFloat(despesa.valor), 0);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="loading">
|
||||||
|
<div>Carregando despesas...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="despesas fade-in">
|
||||||
|
<div className="page-header">
|
||||||
|
<div>
|
||||||
|
<h1>Despesas</h1>
|
||||||
|
<p>Controle todos os gastos da empresa</p>
|
||||||
|
</div>
|
||||||
|
<div className="header-actions">
|
||||||
|
<ViewToggle
|
||||||
|
viewMode={viewMode}
|
||||||
|
onViewModeChange={setViewMode}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => setShowTipoModal(true)}
|
||||||
|
>
|
||||||
|
<FiTag />
|
||||||
|
Tipos de Despesa
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
>
|
||||||
|
<FiPlus />
|
||||||
|
Nova Despesa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Resumo */}
|
||||||
|
<div className="expenses-summary">
|
||||||
|
<div className="summary-card">
|
||||||
|
<div className="summary-icon">
|
||||||
|
<FiDollarSign />
|
||||||
|
</div>
|
||||||
|
<div className="summary-content">
|
||||||
|
<div className="summary-value">
|
||||||
|
R$ {totalDespesas.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
|
</div>
|
||||||
|
<div className="summary-label">Total em Despesas</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Barra de Pesquisa */}
|
||||||
|
<div className="search-box">
|
||||||
|
<FiSearch className="search-icon" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Pesquisar despesas..."
|
||||||
|
className="search-input"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lista de Despesas */}
|
||||||
|
{filteredDespesas.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<FiDollarSign size={48} />
|
||||||
|
<h3>Nenhuma despesa encontrada</h3>
|
||||||
|
<p>Comece registrando sua primeira despesa</p>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
>
|
||||||
|
<FiPlus />
|
||||||
|
Adicionar Despesa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="expenses-table">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Fornecedor</th>
|
||||||
|
<th>Descrição</th>
|
||||||
|
<th>Valor</th>
|
||||||
|
<th>Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredDespesas.map((despesa) => (
|
||||||
|
<tr key={despesa.id}>
|
||||||
|
<td>
|
||||||
|
<div className="expense-date">
|
||||||
|
<FiCalendar className="date-icon" />
|
||||||
|
{new Date(despesa.data).toLocaleDateString('pt-BR')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="badge badge-info">
|
||||||
|
{despesa.tipo_nome}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{despesa.fornecedor_nome || '-'}</td>
|
||||||
|
<td>
|
||||||
|
<div className="expense-description">
|
||||||
|
{despesa.descricao || '-'}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="expense-value">
|
||||||
|
R$ {parseFloat(despesa.valor).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="table-actions">
|
||||||
|
<button
|
||||||
|
className="btn-icon"
|
||||||
|
onClick={() => handleEdit(despesa)}
|
||||||
|
title="Editar"
|
||||||
|
>
|
||||||
|
<FiEdit />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-icon btn-danger"
|
||||||
|
onClick={() => handleDelete(despesa.id)}
|
||||||
|
title="Excluir"
|
||||||
|
>
|
||||||
|
<FiTrash2 />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal de Despesa */}
|
||||||
|
{showModal && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2 className="modal-title">
|
||||||
|
{editingExpense ? 'Editar Despesa' : 'Nova Despesa'}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
className="modal-close"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setEditingExpense(null);
|
||||||
|
setFormData({
|
||||||
|
tipo_despesa: '',
|
||||||
|
fornecedor: '',
|
||||||
|
data: new Date().toISOString().split('T')[0],
|
||||||
|
valor: '',
|
||||||
|
descricao: ''
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="grid grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Tipo de Despesa *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.tipo_despesa}
|
||||||
|
onChange={(e) => setFormData({...formData, tipo_despesa: e.target.value})}
|
||||||
|
placeholder="Ex: Aluguel, Energia, Marketing..."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Fornecedor</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.fornecedor}
|
||||||
|
onChange={(e) => setFormData({...formData, fornecedor: e.target.value})}
|
||||||
|
placeholder="Nome do fornecedor..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Data *</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.data}
|
||||||
|
onChange={(e) => setFormData({...formData, data: e.target.value})}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Valor *</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.valor}
|
||||||
|
onChange={(e) => setFormData({...formData, valor: e.target.value})}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Descrição</label>
|
||||||
|
<textarea
|
||||||
|
className="form-textarea"
|
||||||
|
value={formData.descricao}
|
||||||
|
onChange={(e) => setFormData({...formData, descricao: e.target.value})}
|
||||||
|
placeholder="Descreva a despesa..."
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setEditingExpense(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
{editingExpense ? 'Atualizar' : 'Cadastrar'} Despesa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal de Tipo de Despesa */}
|
||||||
|
{showTipoModal && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal modal-sm">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2 className="modal-title">Novo Tipo de Despesa</h2>
|
||||||
|
<button
|
||||||
|
className="modal-close"
|
||||||
|
onClick={() => {
|
||||||
|
setShowTipoModal(false);
|
||||||
|
setNovoTipo('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleAddTipo}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Nome do Tipo *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
value={novoTipo}
|
||||||
|
onChange={(e) => setNovoTipo(e.target.value)}
|
||||||
|
placeholder="Ex: Aluguel, Energia, Marketing..."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowTipoModal(false);
|
||||||
|
setNovoTipo('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
Criar Tipo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Lista de tipos existentes */}
|
||||||
|
{tiposDespesas.length > 0 && (
|
||||||
|
<div className="existing-types">
|
||||||
|
<h4>Tipos Existentes:</h4>
|
||||||
|
<div className="types-list">
|
||||||
|
{tiposDespesas.map((tipo) => (
|
||||||
|
<span key={tipo.id} className="badge badge-info">
|
||||||
|
{tipo.nome}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Despesas;
|
||||||
739
client/src/pages/Devolucoes.js
Normal file
739
client/src/pages/Devolucoes.js
Normal file
@@ -0,0 +1,739 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
FiArrowLeft,
|
||||||
|
FiPackage,
|
||||||
|
FiCalendar,
|
||||||
|
FiUser,
|
||||||
|
FiDollarSign,
|
||||||
|
FiRotateCcw,
|
||||||
|
FiCheck,
|
||||||
|
FiX,
|
||||||
|
FiAlertTriangle,
|
||||||
|
FiPlus
|
||||||
|
} from 'react-icons/fi';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import '../styles/devolucoes.css';
|
||||||
|
|
||||||
|
const Devolucoes = () => {
|
||||||
|
const [vendas, setVendas] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [vendaSelecionada, setVendaSelecionada] = useState(null);
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [showHistoricoModal, setShowHistoricoModal] = useState(false);
|
||||||
|
const [historicoVenda, setHistoricoVenda] = useState([]);
|
||||||
|
const [itensDevolucao, setItensDevolucao] = useState([]);
|
||||||
|
const [motivo, setMotivo] = useState('');
|
||||||
|
const [processando, setProcessando] = useState(false);
|
||||||
|
const [tipoOperacao, setTipoOperacao] = useState('devolucao');
|
||||||
|
const [produtos, setProdutos] = useState([]);
|
||||||
|
const [itensTroca, setItensTroca] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
carregarVendas();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const carregarVendas = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/devolucoes/vendas');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setVendas(data);
|
||||||
|
} else {
|
||||||
|
toast.error('Erro ao carregar vendas');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar vendas:', error);
|
||||||
|
toast.error('Erro ao carregar vendas');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const carregarHistoricoVenda = async (vendaId) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/devolucoes/venda/${vendaId}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const historico = await response.json();
|
||||||
|
setHistoricoVenda(historico);
|
||||||
|
setShowHistoricoModal(true);
|
||||||
|
} else {
|
||||||
|
toast.error('Erro ao carregar histórico');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar histórico:', error);
|
||||||
|
toast.error('Erro ao carregar histórico');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const carregarProdutos = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/devolucoes/produtos');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setProdutos(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar produtos:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const abrirModalDevolucao = (venda) => {
|
||||||
|
setVendaSelecionada(venda);
|
||||||
|
setItensDevolucao(venda.itens.map(item => ({
|
||||||
|
...item,
|
||||||
|
quantidade_devolver: 0,
|
||||||
|
selecionado: false
|
||||||
|
})));
|
||||||
|
setItensTroca([]);
|
||||||
|
setMotivo('');
|
||||||
|
setTipoOperacao('devolucao');
|
||||||
|
carregarProdutos();
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleItemChange = (itemId, quantidade) => {
|
||||||
|
setItensDevolucao(prev => prev.map(item => {
|
||||||
|
if (item.id === itemId) {
|
||||||
|
const quantidadeDevolver = Math.min(Math.max(0, parseInt(quantidade) || 0), item.quantidade);
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
quantidade_devolver: quantidadeDevolver,
|
||||||
|
selecionado: quantidadeDevolver > 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcularValorDevolucao = () => {
|
||||||
|
return itensDevolucao.reduce((total, item) => {
|
||||||
|
if (item.selecionado && item.quantidade_devolver > 0) {
|
||||||
|
return total + (parseFloat(item.valor_unitario) * item.quantidade_devolver);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcularValorTroca = () => {
|
||||||
|
return itensTroca.reduce((total, item) => {
|
||||||
|
return total + (parseFloat(item.valor_unitario || 0) * parseInt(item.quantidade || 0));
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const adicionarItemTroca = () => {
|
||||||
|
setItensTroca(prev => [...prev, {
|
||||||
|
id: Date.now(),
|
||||||
|
produto_id: '',
|
||||||
|
variacao_id: '',
|
||||||
|
quantidade: 1,
|
||||||
|
valor_unitario: 0,
|
||||||
|
produto_nome: '',
|
||||||
|
variacao_info: ''
|
||||||
|
}]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removerItemTroca = (id) => {
|
||||||
|
setItensTroca(prev => prev.filter(item => item.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTrocaChange = (id, field, value) => {
|
||||||
|
setItensTroca(prev => prev.map(item => {
|
||||||
|
if (item.id === id) {
|
||||||
|
if (field === 'produto_id') {
|
||||||
|
const produto = produtos.find(p => p.id === value);
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
produto_id: value,
|
||||||
|
produto_nome: produto ? `${produto.marca} - ${produto.nome}` : '',
|
||||||
|
variacao_id: '',
|
||||||
|
variacao_info: '',
|
||||||
|
valor_unitario: produto ? produto.valor_revenda : 0
|
||||||
|
};
|
||||||
|
} else if (field === 'variacao_id') {
|
||||||
|
const produto = produtos.find(p => p.id === item.produto_id);
|
||||||
|
const variacao = produto?.variacoes.find(v => v.id === value);
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
variacao_id: value,
|
||||||
|
variacao_info: variacao ? `${variacao.tamanho} - ${variacao.cor}` : '',
|
||||||
|
valor_unitario: variacao ? variacao.preco_venda || produto.valor_revenda : item.valor_unitario
|
||||||
|
};
|
||||||
|
} else if (field === 'quantidade') {
|
||||||
|
const produto = produtos.find(p => p.id === item.produto_id);
|
||||||
|
const variacao = produto?.variacoes.find(v => v.id === item.variacao_id);
|
||||||
|
const quantidadeSolicitada = parseInt(value) || 0;
|
||||||
|
|
||||||
|
if (variacao && quantidadeSolicitada > variacao.quantidade) {
|
||||||
|
toast.error(`Estoque insuficiente! Disponível: ${variacao.quantidade}, solicitado: ${quantidadeSolicitada}`);
|
||||||
|
return { ...item, [field]: variacao.quantidade };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...item, [field]: value };
|
||||||
|
} else {
|
||||||
|
return { ...item, [field]: value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const processarDevolucao = async () => {
|
||||||
|
const itensSelecionados = itensDevolucao.filter(item => item.selecionado && item.quantidade_devolver > 0);
|
||||||
|
|
||||||
|
if (tipoOperacao === 'devolucao' && itensSelecionados.length === 0) {
|
||||||
|
toast.error('Selecione pelo menos um item para devolução');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tipoOperacao === 'troca') {
|
||||||
|
if (itensSelecionados.length === 0) {
|
||||||
|
toast.error('Selecione pelo menos um item para devolver na troca');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itensValidosTroca = itensTroca.filter(item =>
|
||||||
|
item.produto_id && item.variacao_id && item.quantidade > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (itensValidosTroca.length === 0) {
|
||||||
|
toast.error('Adicione pelo menos um produto para troca');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!motivo.trim()) {
|
||||||
|
toast.error(`Informe o motivo da ${tipoOperacao}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setProcessando(true);
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
venda_id: vendaSelecionada.id,
|
||||||
|
tipo_operacao: tipoOperacao,
|
||||||
|
motivo: motivo.trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (itensSelecionados.length > 0) {
|
||||||
|
requestBody.itens_devolucao = itensSelecionados.map(item => ({
|
||||||
|
item_id: item.id,
|
||||||
|
quantidade_devolvida: item.quantidade_devolver
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tipoOperacao === 'troca' && itensTroca.length > 0) {
|
||||||
|
requestBody.itens_troca = itensTroca
|
||||||
|
.filter(item => item.produto_id && item.variacao_id && item.quantidade > 0)
|
||||||
|
.map(item => ({
|
||||||
|
produto_id: item.produto_id,
|
||||||
|
variacao_id: item.variacao_id,
|
||||||
|
quantidade: parseInt(item.quantidade),
|
||||||
|
valor_unitario: parseFloat(item.valor_unitario)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/devolucoes', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
toast.success(result.message);
|
||||||
|
setShowModal(false);
|
||||||
|
carregarVendas();
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
toast.error(error.message || `Erro ao processar ${tipoOperacao}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erro ao processar ${tipoOperacao}:`, error);
|
||||||
|
toast.error(`Erro ao processar ${tipoOperacao}`);
|
||||||
|
} finally {
|
||||||
|
setProcessando(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatarMoeda = (valor) => {
|
||||||
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BRL'
|
||||||
|
}).format(valor);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatarData = (data) => {
|
||||||
|
return new Date(data).toLocaleDateString('pt-BR');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="loading-container">
|
||||||
|
<div className="loading-spinner"></div>
|
||||||
|
<p>Carregando vendas...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="devolucoes-page">
|
||||||
|
<div className="page-header">
|
||||||
|
<div className="header-content">
|
||||||
|
<div className="header-info">
|
||||||
|
<h1>
|
||||||
|
<FiRotateCcw className="page-icon" />
|
||||||
|
Devolução/Troca
|
||||||
|
</h1>
|
||||||
|
<p>Gerencie devoluções e trocas de produtos vendidos</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="vendas-container">
|
||||||
|
{vendas.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<FiPackage className="empty-icon" />
|
||||||
|
<h3>Nenhuma venda encontrada</h3>
|
||||||
|
<p>Não há vendas dos últimos 30 dias disponíveis para devolução</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="vendas-grid">
|
||||||
|
{vendas.map(venda => (
|
||||||
|
<div key={venda.id} className="venda-card">
|
||||||
|
<div className="venda-header">
|
||||||
|
<div className="venda-info">
|
||||||
|
<h3>Venda #{venda.id_venda || venda.id.slice(-8)}</h3>
|
||||||
|
<div className="venda-meta">
|
||||||
|
<span className="venda-data">
|
||||||
|
<FiCalendar />
|
||||||
|
{formatarData(venda.data_venda)}
|
||||||
|
</span>
|
||||||
|
<span className="venda-cliente">
|
||||||
|
<FiUser />
|
||||||
|
{venda.cliente_nome}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="venda-valor">
|
||||||
|
{formatarMoeda(venda.valor_total)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="venda-itens">
|
||||||
|
<h4>Produtos ({venda.itens.length})</h4>
|
||||||
|
<div className="itens-lista">
|
||||||
|
{venda.itens.slice(0, 3).map(item => (
|
||||||
|
<div key={item.id} className="item-resumo">
|
||||||
|
<div className="item-info-completa">
|
||||||
|
<span className="item-nome">{item.produto_nome}</span>
|
||||||
|
<span className="item-variacao">{item.variacao_info}</span>
|
||||||
|
<div className="item-detalhes">
|
||||||
|
<span className="item-quantidade">Qtd: {item.quantidade}</span>
|
||||||
|
<span className="item-valor">R$ {parseFloat(item.valor_unitario).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{venda.itens.length > 3 && (
|
||||||
|
<div className="item-resumo mais-itens">
|
||||||
|
+{venda.itens.length - 3} produtos
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="venda-actions">
|
||||||
|
<button
|
||||||
|
className="btn-detalhes"
|
||||||
|
onClick={() => carregarHistoricoVenda(venda.id)}
|
||||||
|
title="Ver histórico de devoluções/trocas"
|
||||||
|
>
|
||||||
|
<FiPackage />
|
||||||
|
Detalhes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-devolucao"
|
||||||
|
onClick={() => abrirModalDevolucao(venda)}
|
||||||
|
>
|
||||||
|
<FiRotateCcw />
|
||||||
|
Devolução/Troca
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal de Devolução */}
|
||||||
|
{showModal && vendaSelecionada && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal-devolucao">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>
|
||||||
|
<FiRotateCcw />
|
||||||
|
Devolução/Troca
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
className="modal-close"
|
||||||
|
onClick={() => setShowModal(false)}
|
||||||
|
>
|
||||||
|
<FiX />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="venda-info-modal">
|
||||||
|
<h3>Venda #{vendaSelecionada.id_venda || vendaSelecionada.id.slice(-8)}</h3>
|
||||||
|
<div className="info-grid">
|
||||||
|
<div className="info-item">
|
||||||
|
<FiCalendar />
|
||||||
|
<span>{formatarData(vendaSelecionada.data_venda)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="info-item">
|
||||||
|
<FiUser />
|
||||||
|
<span>{vendaSelecionada.cliente_nome}</span>
|
||||||
|
</div>
|
||||||
|
<div className="info-item">
|
||||||
|
<FiDollarSign />
|
||||||
|
<span>{formatarMoeda(vendaSelecionada.valor_total)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tipo-operacao">
|
||||||
|
<h4>Tipo de Operação:</h4>
|
||||||
|
<div className="radio-group">
|
||||||
|
<label className="radio-option">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value="devolucao"
|
||||||
|
checked={tipoOperacao === 'devolucao'}
|
||||||
|
onChange={(e) => setTipoOperacao(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span>Devolução (retorno do dinheiro)</span>
|
||||||
|
</label>
|
||||||
|
<label className="radio-option">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value="troca"
|
||||||
|
checked={tipoOperacao === 'troca'}
|
||||||
|
onChange={(e) => setTipoOperacao(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span>Troca (por outro produto/tamanho)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="itens-devolucao">
|
||||||
|
<h4>Selecione os itens para devolução:</h4>
|
||||||
|
<div className="itens-lista-modal">
|
||||||
|
{itensDevolucao.map(item => (
|
||||||
|
<div key={item.id} className="item-devolucao">
|
||||||
|
<div className="item-info">
|
||||||
|
{item.produto_foto && (
|
||||||
|
<div className="item-foto">
|
||||||
|
<img src={item.produto_foto} alt={item.produto_nome} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="item-dados">
|
||||||
|
<div className="item-nome">{item.produto_nome}</div>
|
||||||
|
<div className="item-codigo">Código: {item.produto_codigo || 'N/A'}</div>
|
||||||
|
<div className="item-detalhes">
|
||||||
|
<span className="variacao-badge">{item.variacao_info}</span>
|
||||||
|
<span className="valor-badge">{formatarMoeda(item.valor_unitario)}</span>
|
||||||
|
<span className="quantidade-badge">Qtd: {item.quantidade}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="item-controles">
|
||||||
|
<label>Qtd. a devolver:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max={item.quantidade}
|
||||||
|
value={item.quantidade_devolver}
|
||||||
|
onChange={(e) => handleItemChange(item.id, e.target.value)}
|
||||||
|
className="quantidade-input"
|
||||||
|
/>
|
||||||
|
<div className="valor-item">
|
||||||
|
{item.quantidade_devolver > 0 && (
|
||||||
|
<span className="valor-devolucao">
|
||||||
|
= {formatarMoeda(item.valor_unitario * item.quantidade_devolver)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Seção de Troca */}
|
||||||
|
{tipoOperacao === 'troca' && (
|
||||||
|
<div className="itens-troca">
|
||||||
|
<div className="troca-header">
|
||||||
|
<h4>Produtos para Troca:</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-adicionar-item"
|
||||||
|
onClick={adicionarItemTroca}
|
||||||
|
>
|
||||||
|
<FiPlus />
|
||||||
|
Adicionar Produto
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lista-troca">
|
||||||
|
{itensTroca.map(item => (
|
||||||
|
<div key={item.id} className="item-troca">
|
||||||
|
<div className="troca-selects">
|
||||||
|
<div className="select-group">
|
||||||
|
<label>Produto:</label>
|
||||||
|
<select
|
||||||
|
value={item.produto_id}
|
||||||
|
onChange={(e) => handleTrocaChange(item.id, 'produto_id', e.target.value)}
|
||||||
|
className="select-produto"
|
||||||
|
>
|
||||||
|
<option value="">Selecione um produto</option>
|
||||||
|
{produtos.map(produto => (
|
||||||
|
<option key={produto.id} value={produto.id}>
|
||||||
|
{produto.marca} - {produto.nome}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.produto_id && (
|
||||||
|
<div className="select-group">
|
||||||
|
<label>Variação:</label>
|
||||||
|
<select
|
||||||
|
value={item.variacao_id}
|
||||||
|
onChange={(e) => handleTrocaChange(item.id, 'variacao_id', e.target.value)}
|
||||||
|
className="select-variacao"
|
||||||
|
>
|
||||||
|
<option value="">Selecione uma variação</option>
|
||||||
|
{produtos.find(p => p.id === item.produto_id)?.variacoes.map(variacao => (
|
||||||
|
<option key={variacao.id} value={variacao.id}>
|
||||||
|
{variacao.tamanho} - {variacao.cor} (Estoque: {variacao.quantidade})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="select-group">
|
||||||
|
<label>Quantidade:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={item.quantidade}
|
||||||
|
onChange={(e) => handleTrocaChange(item.id, 'quantidade', e.target.value)}
|
||||||
|
className="input-quantidade"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="select-group">
|
||||||
|
<label>Valor Unit.:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={item.valor_unitario}
|
||||||
|
onChange={(e) => handleTrocaChange(item.id, 'valor_unitario', e.target.value)}
|
||||||
|
className="input-valor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="troca-actions">
|
||||||
|
<div className="valor-total-item">
|
||||||
|
Total: {formatarMoeda(item.valor_unitario * item.quantidade)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-remover-item"
|
||||||
|
onClick={() => removerItemTroca(item.id)}
|
||||||
|
>
|
||||||
|
<FiX />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{itensTroca.length === 0 && (
|
||||||
|
<div className="empty-troca">
|
||||||
|
<p>Nenhum produto adicionado para troca</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="motivo-devolucao">
|
||||||
|
<label>Motivo da {tipoOperacao} *</label>
|
||||||
|
<textarea
|
||||||
|
value={motivo}
|
||||||
|
onChange={(e) => setMotivo(e.target.value)}
|
||||||
|
placeholder={`Descreva o motivo da ${tipoOperacao}...`}
|
||||||
|
rows="3"
|
||||||
|
className="motivo-textarea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="resumo-devolucao">
|
||||||
|
{tipoOperacao === 'devolucao' ? (
|
||||||
|
<>
|
||||||
|
<div className="resumo-item">
|
||||||
|
<span>Valor total da devolução:</span>
|
||||||
|
<span className="valor-total">{formatarMoeda(calcularValorDevolucao())}</span>
|
||||||
|
</div>
|
||||||
|
<div className="resumo-item">
|
||||||
|
<span>Novo valor da venda:</span>
|
||||||
|
<span className="novo-valor">
|
||||||
|
{formatarMoeda(vendaSelecionada.valor_total - calcularValorDevolucao())}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="resumo-item">
|
||||||
|
<span>Valor dos itens devolvidos:</span>
|
||||||
|
<span className="valor-devolucao">{formatarMoeda(calcularValorDevolucao())}</span>
|
||||||
|
</div>
|
||||||
|
<div className="resumo-item">
|
||||||
|
<span>Valor dos itens de troca:</span>
|
||||||
|
<span className="valor-troca">{formatarMoeda(calcularValorTroca())}</span>
|
||||||
|
</div>
|
||||||
|
<div className="resumo-item">
|
||||||
|
<span>Diferença:</span>
|
||||||
|
<span className={`diferenca-valor ${calcularValorTroca() - calcularValorDevolucao() >= 0 ? 'positiva' : 'negativa'}`}>
|
||||||
|
{calcularValorTroca() - calcularValorDevolucao() >= 0 ? '+' : ''}
|
||||||
|
{formatarMoeda(calcularValorTroca() - calcularValorDevolucao())}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="resumo-item">
|
||||||
|
<span>Novo valor da venda:</span>
|
||||||
|
<span className="novo-valor">
|
||||||
|
{formatarMoeda(vendaSelecionada.valor_total + (calcularValorTroca() - calcularValorDevolucao()))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button
|
||||||
|
className="btn-cancelar"
|
||||||
|
onClick={() => setShowModal(false)}
|
||||||
|
disabled={processando}
|
||||||
|
>
|
||||||
|
<FiX />
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-processar"
|
||||||
|
onClick={processarDevolucao}
|
||||||
|
disabled={processando || (tipoOperacao === 'devolucao' && calcularValorDevolucao() === 0)}
|
||||||
|
>
|
||||||
|
{processando ? (
|
||||||
|
<>
|
||||||
|
<div className="spinner-small"></div>
|
||||||
|
Processando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FiCheck />
|
||||||
|
{tipoOperacao === 'devolucao' ? 'Processar Devolução' : 'Processar Troca'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal de Histórico de Devoluções/Trocas */}
|
||||||
|
{showHistoricoModal && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal-content modal-historico">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>📋 Histórico de Devoluções/Trocas</h2>
|
||||||
|
<button
|
||||||
|
className="btn-fechar"
|
||||||
|
onClick={() => setShowHistoricoModal(false)}
|
||||||
|
>
|
||||||
|
<FiX />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-body">
|
||||||
|
{historicoVenda.length === 0 ? (
|
||||||
|
<div className="historico-vazio">
|
||||||
|
<FiPackage size={48} />
|
||||||
|
<h3>Nenhuma devolução ou troca encontrada</h3>
|
||||||
|
<p>Esta venda ainda não teve nenhuma devolução ou troca.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="historico-lista">
|
||||||
|
{historicoVenda.map((evento, index) => (
|
||||||
|
<div key={index} className="historico-item">
|
||||||
|
<div className="historico-header">
|
||||||
|
<div className="evento-tipo">
|
||||||
|
<span className={`badge ${evento.tipo_operacao === 'troca' ? 'badge-warning' : 'badge-info'}`}>
|
||||||
|
{evento.tipo_operacao === 'troca' ? '🔄 TROCA' : '↩️ DEVOLUÇÃO'}
|
||||||
|
</span>
|
||||||
|
<span className="evento-data">
|
||||||
|
📅 {new Date(evento.data_devolucao).toLocaleDateString('pt-BR')} às {new Date(evento.data_devolucao).toLocaleTimeString('pt-BR', {hour: '2-digit', minute: '2-digit'})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="evento-produto">
|
||||||
|
<h4>🔙 Produto {evento.tipo_operacao === 'troca' ? 'Trocado' : 'Devolvido'}:</h4>
|
||||||
|
{evento.produto_info && (
|
||||||
|
<div className="produto-card">
|
||||||
|
<div className="produto-nome">{evento.produto_info.nome}</div>
|
||||||
|
<div className="produto-detalhes">
|
||||||
|
<span>📦 Código: {evento.produto_info.codigo || 'N/A'}</span>
|
||||||
|
<span>📏 Variação: {evento.produto_info.variacao}</span>
|
||||||
|
<span>🔢 Qtd. Devolvida: {evento.quantidade_devolvida}</span>
|
||||||
|
<span>💸 Valor: R$ {parseFloat(evento.valor_devolucao).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{evento.motivo && (
|
||||||
|
<div className="evento-motivo">
|
||||||
|
<h4>📝 Motivo:</h4>
|
||||||
|
<div className="motivo-texto">{evento.motivo}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button
|
||||||
|
className="btn-cancelar"
|
||||||
|
onClick={() => setShowHistoricoModal(false)}
|
||||||
|
>
|
||||||
|
<FiX />
|
||||||
|
Fechar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Devolucoes;
|
||||||
520
client/src/pages/Emprestimos.js
Normal file
520
client/src/pages/Emprestimos.js
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
FiPlus,
|
||||||
|
FiEdit2,
|
||||||
|
FiTrash2,
|
||||||
|
FiDollarSign,
|
||||||
|
FiCalendar,
|
||||||
|
FiUser,
|
||||||
|
FiCheckCircle,
|
||||||
|
FiClock,
|
||||||
|
FiArrowLeft,
|
||||||
|
FiEye
|
||||||
|
} from 'react-icons/fi';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
const Emprestimos = () => {
|
||||||
|
const [emprestimos, setEmprestimos] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [showDevolucaoModal, setShowDevolucaoModal] = useState(false);
|
||||||
|
const [emprestimoSelecionado, setEmprestimoSelecionado] = useState(null);
|
||||||
|
const [editando, setEditando] = useState(false);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
pessoa: 'Maiara',
|
||||||
|
valor_total: '',
|
||||||
|
descricao: '',
|
||||||
|
data_emprestimo: new Date().toISOString().split('T')[0]
|
||||||
|
});
|
||||||
|
|
||||||
|
const [devolucaoData, setDevolucaoData] = useState({
|
||||||
|
valor_devolvido: '',
|
||||||
|
data_devolucao: new Date().toISOString().split('T')[0],
|
||||||
|
observacoes: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
carregarEmprestimos();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const carregarEmprestimos = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/emprestimos');
|
||||||
|
const data = await response.json();
|
||||||
|
setEmprestimos(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar empréstimos:', error);
|
||||||
|
toast.error('Erro ao carregar empréstimos');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = editando ? `/api/emprestimos/${emprestimoSelecionado.id}` : '/api/emprestimos';
|
||||||
|
const method = editando ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(result.message);
|
||||||
|
setShowModal(false);
|
||||||
|
resetForm();
|
||||||
|
carregarEmprestimos();
|
||||||
|
} else {
|
||||||
|
toast.error(result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao salvar empréstimo:', error);
|
||||||
|
toast.error('Erro ao salvar empréstimo');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDevolucao = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/emprestimos/${emprestimoSelecionado.id}/devolucoes`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(devolucaoData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(result.message);
|
||||||
|
setShowDevolucaoModal(false);
|
||||||
|
setDevolucaoData({
|
||||||
|
valor_devolvido: '',
|
||||||
|
data_devolucao: new Date().toISOString().split('T')[0],
|
||||||
|
observacoes: ''
|
||||||
|
});
|
||||||
|
carregarEmprestimos();
|
||||||
|
} else {
|
||||||
|
toast.error(result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao registrar devolução:', error);
|
||||||
|
toast.error('Erro ao registrar devolução');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const excluirEmprestimo = async (id) => {
|
||||||
|
if (!window.confirm('Tem certeza que deseja excluir este empréstimo?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/emprestimos/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(result.message);
|
||||||
|
carregarEmprestimos();
|
||||||
|
} else {
|
||||||
|
toast.error(result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao excluir empréstimo:', error);
|
||||||
|
toast.error('Erro ao excluir empréstimo');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
pessoa: 'Maiara',
|
||||||
|
valor_total: '',
|
||||||
|
descricao: '',
|
||||||
|
data_emprestimo: new Date().toISOString().split('T')[0]
|
||||||
|
});
|
||||||
|
setEditando(false);
|
||||||
|
setEmprestimoSelecionado(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const abrirModalEdicao = (emprestimo) => {
|
||||||
|
setFormData({
|
||||||
|
pessoa: emprestimo.pessoa,
|
||||||
|
valor_total: emprestimo.valor_total,
|
||||||
|
descricao: emprestimo.descricao || '',
|
||||||
|
data_emprestimo: emprestimo.data_emprestimo
|
||||||
|
});
|
||||||
|
setEmprestimoSelecionado(emprestimo);
|
||||||
|
setEditando(true);
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const abrirModalDevolucao = (emprestimo) => {
|
||||||
|
setEmprestimoSelecionado(emprestimo);
|
||||||
|
setDevolucaoData({
|
||||||
|
valor_devolvido: emprestimo.valor_restante.toString(),
|
||||||
|
data_devolucao: new Date().toISOString().split('T')[0],
|
||||||
|
observacoes: ''
|
||||||
|
});
|
||||||
|
setShowDevolucaoModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatarMoeda = (valor) => {
|
||||||
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BRL'
|
||||||
|
}).format(valor);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatarData = (data) => {
|
||||||
|
return new Date(data + 'T00:00:00').toLocaleDateString('pt-BR');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status) => {
|
||||||
|
const badges = {
|
||||||
|
ativo: { color: 'orange', icon: FiClock, text: 'Ativo' },
|
||||||
|
quitado: { color: 'green', icon: FiCheckCircle, text: 'Quitado' },
|
||||||
|
cancelado: { color: 'red', icon: FiTrash2, text: 'Cancelado' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const badge = badges[status] || badges.ativo;
|
||||||
|
const Icon = badge.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`badge badge-${badge.color}`}>
|
||||||
|
<Icon size={12} />
|
||||||
|
{badge.text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcularTotalEmprestado = () => {
|
||||||
|
return emprestimos
|
||||||
|
.filter(emp => emp.status === 'ativo')
|
||||||
|
.reduce((total, emp) => total + parseFloat(emp.valor_restante), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcularTotalQuitado = () => {
|
||||||
|
return emprestimos
|
||||||
|
.filter(emp => emp.status === 'quitado')
|
||||||
|
.reduce((total, emp) => total + parseFloat(emp.valor_total), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="loading">
|
||||||
|
<div>Carregando empréstimos...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="emprestimos fade-in">
|
||||||
|
<div className="page-header">
|
||||||
|
<div>
|
||||||
|
<h1>Controle de Empréstimos</h1>
|
||||||
|
<p>Gerencie os empréstimos da Maiara</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
>
|
||||||
|
<FiPlus /> Novo Empréstimo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards de Resumo */}
|
||||||
|
<div className="stats-grid">
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-icon orange">
|
||||||
|
<FiDollarSign />
|
||||||
|
</div>
|
||||||
|
<div className="stat-content">
|
||||||
|
<h3>Total em Aberto</h3>
|
||||||
|
<p className="stat-value">{formatarMoeda(calcularTotalEmprestado())}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-icon green">
|
||||||
|
<FiCheckCircle />
|
||||||
|
</div>
|
||||||
|
<div className="stat-content">
|
||||||
|
<h3>Total Quitado</h3>
|
||||||
|
<p className="stat-value">{formatarMoeda(calcularTotalQuitado())}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-icon blue">
|
||||||
|
<FiUser />
|
||||||
|
</div>
|
||||||
|
<div className="stat-content">
|
||||||
|
<h3>Empréstimos Ativos</h3>
|
||||||
|
<p className="stat-value">{emprestimos.filter(emp => emp.status === 'ativo').length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lista de Empréstimos */}
|
||||||
|
<div className="content-card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h2>Lista de Empréstimos</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{emprestimos.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<FiDollarSign size={48} />
|
||||||
|
<h3>Nenhum empréstimo encontrado</h3>
|
||||||
|
<p>Clique em "Novo Empréstimo" para começar</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="table-container">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Pessoa</th>
|
||||||
|
<th>Descrição</th>
|
||||||
|
<th>Valor Total</th>
|
||||||
|
<th>Valor Restante</th>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{emprestimos.map((emprestimo) => (
|
||||||
|
<tr key={emprestimo.id}>
|
||||||
|
<td>
|
||||||
|
<div className="user-info">
|
||||||
|
<FiUser className="user-icon" />
|
||||||
|
<strong>{emprestimo.pessoa}</strong>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="description-text">
|
||||||
|
{emprestimo.descricao || '-'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>{formatarMoeda(emprestimo.valor_total)}</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={emprestimo.valor_restante > 0 ? 'text-orange' : 'text-green'}>
|
||||||
|
<strong>{formatarMoeda(emprestimo.valor_restante)}</strong>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{formatarData(emprestimo.data_emprestimo)}</td>
|
||||||
|
<td>{getStatusBadge(emprestimo.status)}</td>
|
||||||
|
<td>
|
||||||
|
<div className="action-buttons">
|
||||||
|
{emprestimo.status === 'ativo' && emprestimo.valor_restante > 0 && (
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-success"
|
||||||
|
onClick={() => abrirModalDevolucao(emprestimo)}
|
||||||
|
title="Registrar Devolução"
|
||||||
|
>
|
||||||
|
<FiDollarSign />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-outline"
|
||||||
|
onClick={() => abrirModalEdicao(emprestimo)}
|
||||||
|
title="Editar"
|
||||||
|
>
|
||||||
|
<FiEdit2 />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-danger"
|
||||||
|
onClick={() => excluirEmprestimo(emprestimo.id)}
|
||||||
|
title="Excluir"
|
||||||
|
>
|
||||||
|
<FiTrash2 />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Novo/Editar Empréstimo */}
|
||||||
|
{showModal && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h3>{editando ? 'Editar Empréstimo' : 'Novo Empréstimo'}</h3>
|
||||||
|
<button
|
||||||
|
className="modal-close"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
resetForm();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="form-grid">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Pessoa</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.pessoa}
|
||||||
|
onChange={(e) => setFormData({...formData, pessoa: e.target.value})}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Valor Total</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.valor_total}
|
||||||
|
onChange={(e) => setFormData({...formData, valor_total: e.target.value})}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Data do Empréstimo</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.data_emprestimo}
|
||||||
|
onChange={(e) => setFormData({...formData, data_emprestimo: e.target.value})}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group form-group-full">
|
||||||
|
<label className="form-label">Descrição</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
rows="3"
|
||||||
|
value={formData.descricao}
|
||||||
|
onChange={(e) => setFormData({...formData, descricao: e.target.value})}
|
||||||
|
placeholder="Motivo do empréstimo (opcional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
resetForm();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
{editando ? 'Atualizar' : 'Criar'} Empréstimo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal Devolução */}
|
||||||
|
{showDevolucaoModal && emprestimoSelecionado && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h3>Registrar Devolução</h3>
|
||||||
|
<button
|
||||||
|
className="modal-close"
|
||||||
|
onClick={() => setShowDevolucaoModal(false)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleDevolucao}>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="emprestimo-info">
|
||||||
|
<h4>{emprestimoSelecionado.pessoa}</h4>
|
||||||
|
<p>Valor restante: <strong>{formatarMoeda(emprestimoSelecionado.valor_restante)}</strong></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-grid">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Valor da Devolução</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
className="form-input"
|
||||||
|
value={devolucaoData.valor_devolvido}
|
||||||
|
onChange={(e) => setDevolucaoData({...devolucaoData, valor_devolvido: e.target.value})}
|
||||||
|
max={emprestimoSelecionado.valor_restante}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<small className="form-help">
|
||||||
|
Máximo: {formatarMoeda(emprestimoSelecionado.valor_restante)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Data da Devolução</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="form-input"
|
||||||
|
value={devolucaoData.data_devolucao}
|
||||||
|
onChange={(e) => setDevolucaoData({...devolucaoData, data_devolucao: e.target.value})}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group form-group-full">
|
||||||
|
<label className="form-label">Observações</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
rows="3"
|
||||||
|
value={devolucaoData.observacoes}
|
||||||
|
onChange={(e) => setDevolucaoData({...devolucaoData, observacoes: e.target.value})}
|
||||||
|
placeholder="Observações sobre a devolução (opcional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline"
|
||||||
|
onClick={() => setShowDevolucaoModal(false)}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-success">
|
||||||
|
Registrar Devolução
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Emprestimos;
|
||||||
338
client/src/pages/Fornecedores.js
Normal file
338
client/src/pages/Fornecedores.js
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
FiPlus,
|
||||||
|
FiEdit,
|
||||||
|
FiTrash2,
|
||||||
|
FiSearch,
|
||||||
|
FiTruck,
|
||||||
|
FiMail,
|
||||||
|
FiPhone,
|
||||||
|
FiMapPin,
|
||||||
|
FiMessageSquare
|
||||||
|
} from 'react-icons/fi';
|
||||||
|
import { fornecedoresAPI } from '../services/api';
|
||||||
|
import ViewToggle from '../components/ViewToggle';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
const Fornecedores = () => {
|
||||||
|
const [fornecedores, setFornecedores] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [viewMode, setViewMode] = useState('list');
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [editingSupplier, setEditingSupplier] = useState(null);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
razao_social: '',
|
||||||
|
telefone: '',
|
||||||
|
whatsapp: '',
|
||||||
|
endereco: '',
|
||||||
|
email: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
carregarFornecedores();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const carregarFornecedores = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fornecedoresAPI.listar();
|
||||||
|
setFornecedores(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar fornecedores:', error);
|
||||||
|
toast.error('Erro ao carregar fornecedores');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
if (editingSupplier) {
|
||||||
|
await fornecedoresAPI.atualizar(editingSupplier.id, formData);
|
||||||
|
toast.success('Fornecedor atualizado com sucesso!');
|
||||||
|
} else {
|
||||||
|
await fornecedoresAPI.criar(formData);
|
||||||
|
toast.success('Fornecedor cadastrado com sucesso!');
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowModal(false);
|
||||||
|
setEditingSupplier(null);
|
||||||
|
setFormData({
|
||||||
|
razao_social: '',
|
||||||
|
telefone: '',
|
||||||
|
whatsapp: '',
|
||||||
|
endereco: '',
|
||||||
|
email: ''
|
||||||
|
});
|
||||||
|
carregarFornecedores();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao salvar fornecedor:', error);
|
||||||
|
toast.error('Erro ao salvar fornecedor');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (fornecedor) => {
|
||||||
|
setEditingSupplier(fornecedor);
|
||||||
|
setFormData({
|
||||||
|
razao_social: fornecedor.razao_social,
|
||||||
|
telefone: fornecedor.telefone || '',
|
||||||
|
whatsapp: fornecedor.whatsapp || '',
|
||||||
|
endereco: fornecedor.endereco || '',
|
||||||
|
email: fornecedor.email || ''
|
||||||
|
});
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (window.confirm('Tem certeza que deseja excluir este fornecedor?')) {
|
||||||
|
try {
|
||||||
|
await fornecedoresAPI.deletar(id);
|
||||||
|
toast.success('Fornecedor excluído com sucesso!');
|
||||||
|
carregarFornecedores();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao excluir fornecedor:', error);
|
||||||
|
toast.error('Erro ao excluir fornecedor');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredFornecedores = fornecedores.filter(fornecedor =>
|
||||||
|
fornecedor.razao_social.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(fornecedor.email && fornecedor.email.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||||
|
(fornecedor.telefone && fornecedor.telefone.includes(searchTerm))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="loading">
|
||||||
|
<div>Carregando fornecedores...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fornecedores fade-in">
|
||||||
|
<div className="page-header">
|
||||||
|
<div>
|
||||||
|
<h1>Fornecedores</h1>
|
||||||
|
<p>Gerencie os fornecedores da loja</p>
|
||||||
|
</div>
|
||||||
|
<div className="header-actions">
|
||||||
|
<ViewToggle
|
||||||
|
viewMode={viewMode}
|
||||||
|
onViewModeChange={setViewMode}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
>
|
||||||
|
<FiPlus />
|
||||||
|
Novo Fornecedor
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Barra de Pesquisa */}
|
||||||
|
<div className="search-box">
|
||||||
|
<FiSearch className="search-icon" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Pesquisar fornecedores..."
|
||||||
|
className="search-input"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lista de Fornecedores */}
|
||||||
|
{filteredFornecedores.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<FiTruck size={48} />
|
||||||
|
<h3>Nenhum fornecedor encontrado</h3>
|
||||||
|
<p>Comece adicionando seu primeiro fornecedor</p>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
>
|
||||||
|
<FiPlus />
|
||||||
|
Adicionar Fornecedor
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="suppliers-grid">
|
||||||
|
{filteredFornecedores.map((fornecedor) => (
|
||||||
|
<div key={fornecedor.id} className="supplier-card">
|
||||||
|
<div className="supplier-header">
|
||||||
|
<div className="supplier-avatar">
|
||||||
|
<FiTruck />
|
||||||
|
</div>
|
||||||
|
<div className="supplier-actions">
|
||||||
|
<button
|
||||||
|
className="btn-icon"
|
||||||
|
onClick={() => handleEdit(fornecedor)}
|
||||||
|
title="Editar"
|
||||||
|
>
|
||||||
|
<FiEdit />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-icon btn-danger"
|
||||||
|
onClick={() => handleDelete(fornecedor.id)}
|
||||||
|
title="Excluir"
|
||||||
|
>
|
||||||
|
<FiTrash2 />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="supplier-info">
|
||||||
|
<h3 className="supplier-name">{fornecedor.razao_social}</h3>
|
||||||
|
|
||||||
|
{fornecedor.email && (
|
||||||
|
<div className="supplier-detail">
|
||||||
|
<FiMail className="detail-icon" />
|
||||||
|
<span>{fornecedor.email}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fornecedor.telefone && (
|
||||||
|
<div className="supplier-detail">
|
||||||
|
<FiPhone className="detail-icon" />
|
||||||
|
<span>{fornecedor.telefone}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fornecedor.whatsapp && (
|
||||||
|
<div className="supplier-detail">
|
||||||
|
<FiMessageSquare className="detail-icon" />
|
||||||
|
<span>{fornecedor.whatsapp}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fornecedor.endereco && (
|
||||||
|
<div className="supplier-detail">
|
||||||
|
<FiMapPin className="detail-icon" />
|
||||||
|
<span>{fornecedor.endereco}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="supplier-meta">
|
||||||
|
<span className="supplier-date">
|
||||||
|
Cadastrado em {new Date(fornecedor.created_at).toLocaleDateString('pt-BR')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal de Fornecedor */}
|
||||||
|
{showModal && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2 className="modal-title">
|
||||||
|
{editingSupplier ? 'Editar Fornecedor' : 'Novo Fornecedor'}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
className="modal-close"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setEditingSupplier(null);
|
||||||
|
setFormData({
|
||||||
|
razao_social: '',
|
||||||
|
telefone: '',
|
||||||
|
whatsapp: '',
|
||||||
|
endereco: '',
|
||||||
|
email: ''
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Razão Social *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.razao_social}
|
||||||
|
onChange={(e) => setFormData({...formData, razao_social: e.target.value})}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Telefone</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.telefone}
|
||||||
|
onChange={(e) => setFormData({...formData, telefone: e.target.value})}
|
||||||
|
placeholder="(11) 3333-3333"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">WhatsApp</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.whatsapp}
|
||||||
|
onChange={(e) => setFormData({...formData, whatsapp: e.target.value})}
|
||||||
|
placeholder="(11) 99999-9999"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">E-mail</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-input"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({...formData, email: e.target.value})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Endereço</label>
|
||||||
|
<textarea
|
||||||
|
className="form-textarea"
|
||||||
|
value={formData.endereco}
|
||||||
|
onChange={(e) => setFormData({...formData, endereco: e.target.value})}
|
||||||
|
placeholder="Rua, número, bairro, cidade, CEP"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setEditingSupplier(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
{editingSupplier ? 'Atualizar' : 'Cadastrar'} Fornecedor
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Fornecedores;
|
||||||
1283
client/src/pages/Produtos.js
Normal file
1283
client/src/pages/Produtos.js
Normal file
File diff suppressed because it is too large
Load Diff
1471
client/src/pages/Vendas.js
Normal file
1471
client/src/pages/Vendas.js
Normal file
File diff suppressed because it is too large
Load Diff
110
client/src/services/api.js
Normal file
110
client/src/services/api.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.NODE_ENV === 'production'
|
||||||
|
? ''
|
||||||
|
: 'http://localhost:5000';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: `${API_BASE_URL}/api`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Interceptor para tratamento de erros
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
console.error('Erro na API:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// === PRODUTOS ===
|
||||||
|
export const produtosAPI = {
|
||||||
|
listar: () => api.get('/produtos'),
|
||||||
|
criar: (produto) => api.post('/produtos', produto),
|
||||||
|
criarComFoto: (formData) => {
|
||||||
|
return api.post('/produtos', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buscarPorId: (id) => api.get(`/produtos/${id}`),
|
||||||
|
atualizar: (id, produto) => api.put(`/produtos/${id}`, produto),
|
||||||
|
atualizarComFoto: (id, formData) => {
|
||||||
|
return api.put(`/produtos/${id}`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deletar: (id) => api.delete(`/produtos/${id}`),
|
||||||
|
|
||||||
|
// Variações
|
||||||
|
listarVariacoes: (produtoId) => api.get(`/produtos/${produtoId}/variacoes`),
|
||||||
|
adicionarVariacao: (produtoId, formData) => {
|
||||||
|
return api.post(`/produtos/${produtoId}/variacoes`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
atualizarVariacao: (produtoId, variacaoId, dados) =>
|
||||||
|
api.put(`/produtos/${produtoId}/variacoes/${variacaoId}`, dados),
|
||||||
|
deletarVariacao: (produtoId, variacaoId) =>
|
||||||
|
api.delete(`/produtos/${produtoId}/variacoes/${variacaoId}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
// === CLIENTES ===
|
||||||
|
export const clientesAPI = {
|
||||||
|
listar: () => api.get('/clientes'),
|
||||||
|
criar: (cliente) => api.post('/clientes', cliente),
|
||||||
|
buscarPorId: (id) => api.get(`/clientes/${id}`),
|
||||||
|
atualizar: (id, cliente) => api.put(`/clientes/${id}`, cliente),
|
||||||
|
deletar: (id) => api.delete(`/clientes/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
// === FORNECEDORES ===
|
||||||
|
export const fornecedoresAPI = {
|
||||||
|
listar: () => api.get('/fornecedores'),
|
||||||
|
criar: (fornecedor) => api.post('/fornecedores', fornecedor),
|
||||||
|
buscarPorId: (id) => api.get(`/fornecedores/${id}`),
|
||||||
|
atualizar: (id, fornecedor) => api.put(`/fornecedores/${id}`, fornecedor),
|
||||||
|
deletar: (id) => api.delete(`/fornecedores/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
// === DESPESAS ===
|
||||||
|
export const despesasAPI = {
|
||||||
|
listar: () => api.get('/despesas'),
|
||||||
|
criar: (despesa) => api.post('/despesas', despesa),
|
||||||
|
buscarPorId: (id) => api.get(`/despesas/${id}`),
|
||||||
|
atualizar: (id, despesa) => api.put(`/despesas/${id}`, despesa),
|
||||||
|
deletar: (id) => api.delete(`/despesas/${id}`),
|
||||||
|
|
||||||
|
// Tipos de despesas
|
||||||
|
listarTipos: () => api.get('/tipos-despesas'),
|
||||||
|
criarTipo: (tipo) => api.post('/tipos-despesas', tipo),
|
||||||
|
deletarTipo: (id) => api.delete(`/tipos-despesas/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
// === VENDAS ===
|
||||||
|
export const vendasAPI = {
|
||||||
|
listar: () => api.get('/vendas'),
|
||||||
|
criar: (venda) => api.post('/vendas', venda),
|
||||||
|
buscarPorId: (id) => api.get(`/vendas/${id}`),
|
||||||
|
atualizar: (id, venda) => api.put(`/vendas/${id}`, venda),
|
||||||
|
deletar: (id) => api.delete(`/vendas/${id}`),
|
||||||
|
|
||||||
|
// Itens da venda
|
||||||
|
adicionarItem: (vendaId, item) => api.post(`/vendas/${vendaId}/itens`, item),
|
||||||
|
removerItem: (vendaId, itemId) => api.delete(`/vendas/${vendaId}/itens/${itemId}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
// === DASHBOARD ===
|
||||||
|
export const dashboardAPI = {
|
||||||
|
obterEstatisticas: () => api.get('/dashboard'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api;
|
||||||
551
client/src/styles/dashboard-contabilidade.css
Normal file
551
client/src/styles/dashboard-contabilidade.css
Normal file
@@ -0,0 +1,551 @@
|
|||||||
|
/* Dashboard Contabilidade Completa */
|
||||||
|
.dashboard {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header p {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 400px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #e5e7eb;
|
||||||
|
border-top: 4px solid #3b82f6;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CONTABILIDADE COMPLETA */
|
||||||
|
.contabilidade-section {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contabilidade-section h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contabilidade-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contabilidade-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
border-left: 4px solid;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contabilidade-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 15px -3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contabilidade-card.receita {
|
||||||
|
border-left-color: #10b981;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contabilidade-card.custo {
|
||||||
|
border-left-color: #f59e0b;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #fffbeb 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contabilidade-card.despesa {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #fef2f2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contabilidade-card.lucro {
|
||||||
|
border-left-color: #3b82f6;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #eff6ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receita .card-icon { color: #10b981; }
|
||||||
|
.custo .card-icon { color: #f59e0b; }
|
||||||
|
.despesa .card-icon { color: #ef4444; }
|
||||||
|
.lucro .card-icon { color: #3b82f6; }
|
||||||
|
|
||||||
|
.card-header span {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RESUMO FINANCEIRO */
|
||||||
|
.resumo-section {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resumo-section h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resumo-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resumo-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resumo-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resumo-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resumo-header span {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resumo-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resumo-icon.success { color: #10b981; }
|
||||||
|
.resumo-icon.danger { color: #ef4444; }
|
||||||
|
.resumo-icon.primary { color: #3b82f6; }
|
||||||
|
|
||||||
|
.resumo-value {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resumo-value.success { color: #10b981; }
|
||||||
|
.resumo-value.danger { color: #ef4444; }
|
||||||
|
.resumo-value.primary { color: #3b82f6; }
|
||||||
|
|
||||||
|
/* VENDAS A PRAZO */
|
||||||
|
.vendas-prazo-section {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vendas-prazo-section h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vendas-prazo-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vendas-prazo-header {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prazo-summary {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prazo-count {
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prazo-total {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vendas-prazo-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venda-prazo-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venda-prazo-item:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venda-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cliente-nome {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venda-valor {
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venda-vencimento {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vencimento-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venda-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-whatsapp {
|
||||||
|
background: #25d366;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-whatsapp:hover {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PARCELAS PENDENTES */
|
||||||
|
.parcelas-section {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcelas-section h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcelas-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-item {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-cliente {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-valor {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-detalhes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 0.25rem;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* EMPRÉSTIMOS */
|
||||||
|
.emprestimos-section {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emprestimos-section h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emprestimos-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emprestimo-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
border-left: 4px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emprestimo-card.aberto {
|
||||||
|
border-left-color: #f59e0b;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #fffbeb 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emprestimo-card.quitado {
|
||||||
|
border-left-color: #10b981;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emprestimo-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emprestimo-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aberto .emprestimo-icon { color: #f59e0b; }
|
||||||
|
.quitado .emprestimo-icon { color: #10b981; }
|
||||||
|
|
||||||
|
.emprestimo-header span {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emprestimo-value {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emprestimo-count {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ESTATÍSTICAS GERAIS */
|
||||||
|
.estatisticas-section {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.estatisticas-section h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.estatisticas-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #3b82f6;
|
||||||
|
background: #eff6ff;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* EMPTY STATE */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: #6b7280;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RESPONSIVIDADE */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dashboard {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contabilidade-grid,
|
||||||
|
.resumo-grid,
|
||||||
|
.emprestimos-grid,
|
||||||
|
.estatisticas-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venda-prazo-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venda-vencimento {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venda-actions {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-detalhes {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
404
client/src/styles/dashboard-simples.css
Normal file
404
client/src/styles/dashboard-simples.css
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
/* Dashboard Simples e Limpo */
|
||||||
|
.dashboard-simples {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #f8fafc;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header p {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 400px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #e2e8f0;
|
||||||
|
border-top: 4px solid #3b82f6;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RESUMO FINANCEIRO PRINCIPAL */
|
||||||
|
.resumo-principal {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-principal {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
border-left: 5px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-principal:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 20px -4px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-principal.receitas {
|
||||||
|
border-left-color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-principal.despesas {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-principal.lucro {
|
||||||
|
border-left-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receitas .card-icon {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.despesas .card-icon {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucro .card-icon {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon svg {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valor-principal {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtexto {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SEÇÕES */
|
||||||
|
.secao-vendas-prazo,
|
||||||
|
.secao-parcelas {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secao-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secao-header h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-prazo,
|
||||||
|
.total-parcelas {
|
||||||
|
background: #f1f5f9;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LISTA DE VENDAS A PRAZO */
|
||||||
|
.lista-vendas-prazo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-venda-prazo {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-venda-prazo:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venda-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cliente {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valor {
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venda-vencimento {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: #64748b;
|
||||||
|
margin: 0 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-whatsapp-simples {
|
||||||
|
background: #25d366;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-whatsapp-simples:hover {
|
||||||
|
background: #22c55e;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-whatsapp-simples svg {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mais-vendas {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-style: italic;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px dashed #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LISTA DE PARCELAS */
|
||||||
|
.lista-parcelas {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-parcela {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-detalhes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 0.25rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ESTATÍSTICAS RÁPIDAS */
|
||||||
|
.estatisticas-rapidas {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #3b82f6;
|
||||||
|
background: #eff6ff;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-numero {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* EMPTY STATE */
|
||||||
|
.empty-recebimentos {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
color: #cbd5e1;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-recebimentos h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-recebimentos p {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RESPONSIVIDADE */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dashboard-simples {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resumo-principal {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-principal {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valor-principal {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-venda-prazo,
|
||||||
|
.item-parcela {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venda-vencimento {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-whatsapp-simples {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-detalhes {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.estatisticas-rapidas {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secao-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
1005
client/src/styles/devolucoes.css
Normal file
1005
client/src/styles/devolucoes.css
Normal file
File diff suppressed because it is too large
Load Diff
156
client/src/styles/pix-integration.css
Normal file
156
client/src/styles/pix-integration.css
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
/* PIX Integration Styles */
|
||||||
|
|
||||||
|
.pix-modal {
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-image {
|
||||||
|
max-width: 200px;
|
||||||
|
height: auto;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-copy-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-code-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-code-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy-pix {
|
||||||
|
background: #00d4aa;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy-pix:hover {
|
||||||
|
background: #00b894;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-info {
|
||||||
|
background: #e8f5e8;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: left;
|
||||||
|
border-left: 4px solid #00d4aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-instructions {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pix {
|
||||||
|
background: #00d4aa;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pix:hover {
|
||||||
|
background: #00b894;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pix:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-status {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-status.pending {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-status.approved {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-status.rejected {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-timer {
|
||||||
|
background: #fff3cd;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 10px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pix-modal {
|
||||||
|
width: 95vw;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-image {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-code-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy-pix {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
595
client/src/styles/vendas-melhorias.css
Normal file
595
client/src/styles/vendas-melhorias.css
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
/* Estilos específicos para melhorias nas vendas */
|
||||||
|
|
||||||
|
/* Botões WhatsApp */
|
||||||
|
.whatsapp-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-success {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #28a745;
|
||||||
|
color: #28a745;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-success:hover {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container das parcelas */
|
||||||
|
.parcelas-container {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-numero {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-valor {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-data .form-label {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input-sm {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade para parcelas */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.parcela-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-data {
|
||||||
|
min-width: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-data input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para coluna de produtos na tabela de vendas */
|
||||||
|
.sale-products {
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-variation {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-qty {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-products {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
padding: 4px;
|
||||||
|
background: #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-products {
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade para tabela de vendas */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.sale-products {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-variation {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sale-products {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-item {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-variation,
|
||||||
|
.product-qty {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Melhorias nos totais */
|
||||||
|
.sale-totals .total-final {
|
||||||
|
background: #e8f5e8;
|
||||||
|
border-color: #28a745;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indicador de valor automático */
|
||||||
|
.valor-automatico {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valor-automatico::after {
|
||||||
|
content: "Auto";
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Seção de informações de pagamento */
|
||||||
|
.payment-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-section h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-info-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Destaque para compra a prazo */
|
||||||
|
.prazo-highlight {
|
||||||
|
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prazo-highlight .form-label {
|
||||||
|
color: #856404;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animações */
|
||||||
|
.parcela-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcela-item:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estados de validação */
|
||||||
|
.form-input.error {
|
||||||
|
border-color: #dc3545;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input.success {
|
||||||
|
border-color: #28a745;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botões de ação adicionais */
|
||||||
|
.btn-icon.btn-info {
|
||||||
|
background: #17a2b8;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon.btn-info:hover {
|
||||||
|
background: #138496;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon.btn-success:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ID da venda */
|
||||||
|
.sale-id {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6c757d;
|
||||||
|
background: #e9ecef;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-quantity {
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-price {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para devoluções/trocas detalhadas */
|
||||||
|
.devolucoes-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devolucao-item-completa {
|
||||||
|
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.devolucao-header-completa {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operacao-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devolucao-data {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.produto-devolvido {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.produto-devolvido h4 {
|
||||||
|
color: #dc3545;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.produto-info-card {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.produto-nome-dev {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.produto-detalhes-dev {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.produto-detalhes-dev span {
|
||||||
|
background: #fff;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valores-dev {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valores-dev span {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motivo-operacao {
|
||||||
|
margin: 1rem 0;
|
||||||
|
background: #e7f3ff;
|
||||||
|
border: 1px solid #b3d9ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motivo-operacao h4 {
|
||||||
|
color: #0066cc;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motivo-texto {
|
||||||
|
background: #fff;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
font-style: italic;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-troca {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.troca-aviso {
|
||||||
|
background: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para itens com status */
|
||||||
|
.item-header-status {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-status-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-devolvido {
|
||||||
|
background: #fff5f5 !important;
|
||||||
|
border-left: 4px solid #dc3545 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-normal {
|
||||||
|
background: #f0fff4 !important;
|
||||||
|
border-left: 4px solid #28a745 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-evento {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
|
||||||
|
border: 1px solid #bbdefb;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evento-info {
|
||||||
|
color: #1565c0;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evento-hora {
|
||||||
|
color: #666;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evento-observacao {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-observacao {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge para status com troca */
|
||||||
|
.badge-warning {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal maior para acomodar mais informações */
|
||||||
|
.modal-lg {
|
||||||
|
max-width: 900px !important;
|
||||||
|
width: 95% !important;
|
||||||
|
max-height: 90vh !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll para itens quando há muitos */
|
||||||
|
.items-view {
|
||||||
|
max-height: 400px !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal de WhatsApp */
|
||||||
|
.whatsapp-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whatsapp-info {
|
||||||
|
background: #e8f5e8;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whatsapp-info p {
|
||||||
|
margin: 4px 0;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-view {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-photo {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
min-width: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
config/google-credentials.example.json
Normal file
13
config/google-credentials.example.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"web": {
|
||||||
|
"client_id": "SEU_CLIENT_ID_AQUI.apps.googleusercontent.com",
|
||||||
|
"project_id": "seu-projeto-id",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_secret": "SUA_CLIENT_SECRET_AQUI",
|
||||||
|
"redirect_uris": [
|
||||||
|
"http://localhost:5000/auth/google/callback"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
295
config/google-drive.js
Normal file
295
config/google-drive.js
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
const { google } = require('googleapis');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class GoogleDriveService {
|
||||||
|
constructor() {
|
||||||
|
this.auth = null;
|
||||||
|
this.drive = null;
|
||||||
|
this.credentialsPath = path.join(__dirname, 'google-credentials.json');
|
||||||
|
this.tokensPath = path.join(__dirname, 'google-tokens.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicializa a autenticação com credenciais salvas no Supabase ou arquivo
|
||||||
|
*/
|
||||||
|
async initializeAuth(credentials = null) {
|
||||||
|
try {
|
||||||
|
let creds = credentials;
|
||||||
|
|
||||||
|
// Se não foram passadas credenciais, tenta carregar do arquivo
|
||||||
|
if (!creds && fs.existsSync(this.credentialsPath)) {
|
||||||
|
creds = JSON.parse(fs.readFileSync(this.credentialsPath, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!creds) {
|
||||||
|
throw new Error('Credenciais do Google não encontradas');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurar OAuth2 com scopes
|
||||||
|
this.auth = new google.auth.OAuth2(
|
||||||
|
creds.client_id,
|
||||||
|
creds.client_secret,
|
||||||
|
creds.redirect_uris[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Definir scopes para Google Drive
|
||||||
|
this.scopes = [
|
||||||
|
'https://www.googleapis.com/auth/drive.file',
|
||||||
|
'https://www.googleapis.com/auth/drive.readonly'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Carregar tokens salvos se existirem
|
||||||
|
if (fs.existsSync(this.tokensPath)) {
|
||||||
|
const tokens = JSON.parse(fs.readFileSync(this.tokensPath, 'utf8'));
|
||||||
|
this.auth.setCredentials(tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar Google Drive API
|
||||||
|
this.drive = google.drive({ version: 'v3', auth: this.auth });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao inicializar Google Drive:', error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gera URL de autenticação para o usuário
|
||||||
|
*/
|
||||||
|
getAuthUrl() {
|
||||||
|
if (!this.auth) {
|
||||||
|
throw new Error('Autenticação não inicializada');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.auth.generateAuthUrl({
|
||||||
|
access_type: 'offline',
|
||||||
|
scope: this.scopes,
|
||||||
|
prompt: 'consent'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processa o código de autorização retornado pelo Google
|
||||||
|
*/
|
||||||
|
async handleAuthCallback(code) {
|
||||||
|
try {
|
||||||
|
const { tokens } = await this.auth.getToken(code);
|
||||||
|
this.auth.setCredentials(tokens);
|
||||||
|
|
||||||
|
// Salvar tokens para uso futuro
|
||||||
|
fs.writeFileSync(this.tokensPath, JSON.stringify(tokens, null, 2));
|
||||||
|
|
||||||
|
console.log('✅ Autenticação Google Drive realizada com sucesso');
|
||||||
|
return tokens;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro na autenticação Google Drive:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se o usuário está autenticado
|
||||||
|
*/
|
||||||
|
isAuthenticated() {
|
||||||
|
return this.auth && this.auth.credentials && this.auth.credentials.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cria uma pasta no Google Drive (se não existir)
|
||||||
|
*/
|
||||||
|
async createFolder(folderName, parentFolderId = null) {
|
||||||
|
try {
|
||||||
|
// Verificar se a pasta já existe
|
||||||
|
const query = `name='${folderName}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
|
||||||
|
const existingFolders = await this.drive.files.list({
|
||||||
|
q: parentFolderId ? `${query} and '${parentFolderId}' in parents` : query,
|
||||||
|
fields: 'files(id, name)'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingFolders.data.files.length > 0) {
|
||||||
|
return existingFolders.data.files[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Criar nova pasta
|
||||||
|
const folderMetadata = {
|
||||||
|
name: folderName,
|
||||||
|
mimeType: 'application/vnd.google-apps.folder',
|
||||||
|
parents: parentFolderId ? [parentFolderId] : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
const folder = await this.drive.files.create({
|
||||||
|
resource: folderMetadata,
|
||||||
|
fields: 'id'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📁 Pasta '${folderName}' criada no Google Drive: ${folder.data.id}`);
|
||||||
|
return folder.data.id;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar pasta no Google Drive:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Faz upload de um arquivo para o Google Drive
|
||||||
|
*/
|
||||||
|
async uploadFile(filePath, fileName, folderId = null, mimeType = 'image/jpeg') {
|
||||||
|
try {
|
||||||
|
if (!this.isAuthenticated()) {
|
||||||
|
throw new Error('Usuário não autenticado no Google Drive');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileMetadata = {
|
||||||
|
name: fileName,
|
||||||
|
parents: folderId ? [folderId] : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
const media = {
|
||||||
|
mimeType: mimeType,
|
||||||
|
body: fs.createReadStream(filePath)
|
||||||
|
};
|
||||||
|
|
||||||
|
const file = await this.drive.files.create({
|
||||||
|
resource: fileMetadata,
|
||||||
|
media: media,
|
||||||
|
fields: 'id, name, webViewLink, webContentLink'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tornar o arquivo público para visualização
|
||||||
|
await this.drive.permissions.create({
|
||||||
|
fileId: file.data.id,
|
||||||
|
resource: {
|
||||||
|
role: 'reader',
|
||||||
|
type: 'anyone'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📤 Arquivo '${fileName}' enviado para Google Drive: ${file.data.id}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: file.data.id,
|
||||||
|
name: file.data.name,
|
||||||
|
webViewLink: file.data.webViewLink,
|
||||||
|
webContentLink: file.data.webContentLink,
|
||||||
|
publicUrl: `https://drive.google.com/uc?id=${file.data.id}`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao fazer upload para Google Drive:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Faz upload de múltiplos arquivos
|
||||||
|
*/
|
||||||
|
async uploadMultipleFiles(files, folderId = null) {
|
||||||
|
const uploadPromises = files.map(file =>
|
||||||
|
this.uploadFile(file.path, file.name, folderId, file.mimeType)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await Promise.all(uploadPromises);
|
||||||
|
console.log(`📤 ${results.length} arquivos enviados para Google Drive`);
|
||||||
|
return results;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao fazer upload múltiplo:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deleta um arquivo do Google Drive
|
||||||
|
*/
|
||||||
|
async deleteFile(fileId) {
|
||||||
|
try {
|
||||||
|
await this.drive.files.delete({
|
||||||
|
fileId: fileId
|
||||||
|
});
|
||||||
|
console.log(`🗑️ Arquivo deletado do Google Drive: ${fileId}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao deletar arquivo do Google Drive:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista arquivos de uma pasta
|
||||||
|
*/
|
||||||
|
async listFiles(folderId = null, pageSize = 10) {
|
||||||
|
try {
|
||||||
|
const query = folderId ? `'${folderId}' in parents and trashed=false` : 'trashed=false';
|
||||||
|
|
||||||
|
const response = await this.drive.files.list({
|
||||||
|
q: query,
|
||||||
|
pageSize: pageSize,
|
||||||
|
fields: 'files(id, name, mimeType, createdTime, size, webViewLink)'
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.files;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao listar arquivos do Google Drive:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se o token está próximo do vencimento
|
||||||
|
*/
|
||||||
|
isTokenExpiringSoon() {
|
||||||
|
if (!this.auth.credentials || !this.auth.credentials.expiry_date) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiryTime = new Date(this.auth.credentials.expiry_date);
|
||||||
|
const now = new Date();
|
||||||
|
const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000);
|
||||||
|
|
||||||
|
return expiryTime <= fiveMinutesFromNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renova o token se necessário
|
||||||
|
*/
|
||||||
|
async refreshTokenIfNeeded() {
|
||||||
|
if (this.isTokenExpiringSoon()) {
|
||||||
|
try {
|
||||||
|
const { credentials } = await this.auth.refreshAccessToken();
|
||||||
|
this.auth.setCredentials(credentials);
|
||||||
|
|
||||||
|
// Salvar tokens atualizados
|
||||||
|
fs.writeFileSync(this.tokensPath, JSON.stringify(credentials, null, 2));
|
||||||
|
console.log('🔄 Token Google Drive renovado automaticamente');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao renovar token Google Drive:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtém informações sobre o espaço de armazenamento
|
||||||
|
*/
|
||||||
|
async getStorageInfo() {
|
||||||
|
try {
|
||||||
|
const response = await this.drive.about.get({
|
||||||
|
fields: 'storageQuota'
|
||||||
|
});
|
||||||
|
|
||||||
|
const quota = response.data.storageQuota;
|
||||||
|
return {
|
||||||
|
limit: parseInt(quota.limit),
|
||||||
|
usage: parseInt(quota.usage),
|
||||||
|
usageInDrive: parseInt(quota.usageInDrive),
|
||||||
|
usageInDriveTrash: parseInt(quota.usageInDriveTrash)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao obter informações de armazenamento:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GoogleDriveService;
|
||||||
384
config/google-sheets.js
Normal file
384
config/google-sheets.js
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
const { google } = require('googleapis');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class GoogleSheetsService {
|
||||||
|
constructor() {
|
||||||
|
this.auth = null;
|
||||||
|
this.sheets = null;
|
||||||
|
this.drive = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar autenticação OAuth 2.0
|
||||||
|
async initializeAuth(credentialsData = null) {
|
||||||
|
try {
|
||||||
|
let credentials;
|
||||||
|
|
||||||
|
if (credentialsData) {
|
||||||
|
// Usar credenciais fornecidas via parâmetro
|
||||||
|
credentials = credentialsData;
|
||||||
|
} else {
|
||||||
|
// Tentar carregar do arquivo (fallback)
|
||||||
|
const credentialsPath = path.join(__dirname, 'google-credentials.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(credentialsPath)) {
|
||||||
|
throw new Error('Credenciais do Google não configuradas. Configure na página de Configurações.');
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { client_id, client_secret, redirect_uris } = credentials.web || credentials;
|
||||||
|
|
||||||
|
this.auth = new google.auth.OAuth2(
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
redirect_uris[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sheets = google.sheets({ version: 'v4', auth: this.auth });
|
||||||
|
this.drive = google.drive({ version: 'v3', auth: this.auth });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao inicializar autenticação Google:', error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gerar URL de autorização
|
||||||
|
getAuthUrl() {
|
||||||
|
const scopes = [
|
||||||
|
'https://www.googleapis.com/auth/spreadsheets',
|
||||||
|
'https://www.googleapis.com/auth/drive.file'
|
||||||
|
];
|
||||||
|
|
||||||
|
return this.auth.generateAuthUrl({
|
||||||
|
access_type: 'offline',
|
||||||
|
scope: scopes,
|
||||||
|
prompt: 'consent'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processar código de autorização
|
||||||
|
async handleAuthCallback(code) {
|
||||||
|
try {
|
||||||
|
const { tokens } = await this.auth.getToken(code);
|
||||||
|
this.auth.setCredentials(tokens);
|
||||||
|
|
||||||
|
// Salvar tokens para uso futuro
|
||||||
|
const tokensPath = path.join(__dirname, 'google-tokens.json');
|
||||||
|
fs.writeFileSync(tokensPath, JSON.stringify(tokens, null, 2));
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao processar callback de autorização:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carregar tokens salvos
|
||||||
|
async loadSavedTokens() {
|
||||||
|
try {
|
||||||
|
const tokensPath = path.join(__dirname, 'google-tokens.json');
|
||||||
|
|
||||||
|
if (fs.existsSync(tokensPath)) {
|
||||||
|
const tokens = JSON.parse(fs.readFileSync(tokensPath, 'utf8'));
|
||||||
|
this.auth.setCredentials(tokens);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar tokens salvos:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Criar ou atualizar planilha persistente
|
||||||
|
async createOrUpdatePersistentSpreadsheet(spreadsheetId = null, title = 'Liberi Kids - Sistema de Estoque') {
|
||||||
|
try {
|
||||||
|
if (spreadsheetId) {
|
||||||
|
// Tentar verificar se planilha existente ainda existe
|
||||||
|
try {
|
||||||
|
const existingSheet = await this.sheets.spreadsheets.get({
|
||||||
|
spreadsheetId: spreadsheetId
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Planilha existente encontrada, usando a mesma...');
|
||||||
|
return {
|
||||||
|
spreadsheetId,
|
||||||
|
url: `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit`,
|
||||||
|
exists: true
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Planilha não encontrada ou inacessível, criando nova...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Criar nova planilha
|
||||||
|
const spreadsheet = await this.sheets.spreadsheets.create({
|
||||||
|
resource: {
|
||||||
|
properties: {
|
||||||
|
title: title
|
||||||
|
},
|
||||||
|
sheets: [
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
sheetId: 0,
|
||||||
|
title: 'Produtos',
|
||||||
|
gridProperties: {
|
||||||
|
rowCount: 1000,
|
||||||
|
columnCount: 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
sheetId: 1,
|
||||||
|
title: 'Vendas',
|
||||||
|
gridProperties: {
|
||||||
|
rowCount: 1000,
|
||||||
|
columnCount: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const newSpreadsheetId = spreadsheet.data.spreadsheetId;
|
||||||
|
|
||||||
|
// Aguardar um pouco para garantir que a planilha foi criada
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// Configurar cabeçalhos
|
||||||
|
await this.setupHeaders(newSpreadsheetId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
spreadsheetId: newSpreadsheetId,
|
||||||
|
url: `https://docs.google.com/spreadsheets/d/${newSpreadsheetId}/edit`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar planilha:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurar cabeçalhos das abas
|
||||||
|
async setupHeaders(spreadsheetId) {
|
||||||
|
try {
|
||||||
|
const produtosHeaders = [
|
||||||
|
'ID da Roupa',
|
||||||
|
'Nome do Produto',
|
||||||
|
'Fornecedor',
|
||||||
|
'Tamanho',
|
||||||
|
'Estação',
|
||||||
|
'Gênero',
|
||||||
|
'Valor da Compra',
|
||||||
|
'Valor da Venda',
|
||||||
|
'Data da Compra',
|
||||||
|
'Data da Venda',
|
||||||
|
'Estoque Atual',
|
||||||
|
'Marca'
|
||||||
|
];
|
||||||
|
|
||||||
|
const vendasHeaders = [
|
||||||
|
'ID da Venda',
|
||||||
|
'Cliente',
|
||||||
|
'Data da Venda',
|
||||||
|
'Tipo de Pagamento',
|
||||||
|
'Valor Total',
|
||||||
|
'Desconto',
|
||||||
|
'Valor Final',
|
||||||
|
'Status',
|
||||||
|
'Observações',
|
||||||
|
'Produtos Vendidos'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Configurar cabeçalhos da aba Produtos
|
||||||
|
await this.sheets.spreadsheets.values.update({
|
||||||
|
spreadsheetId,
|
||||||
|
range: 'Produtos!A1:L1',
|
||||||
|
valueInputOption: 'USER_ENTERED',
|
||||||
|
resource: {
|
||||||
|
values: [produtosHeaders]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configurar cabeçalhos da aba Vendas
|
||||||
|
await this.sheets.spreadsheets.values.update({
|
||||||
|
spreadsheetId,
|
||||||
|
range: 'Vendas!A1:J1',
|
||||||
|
valueInputOption: 'USER_ENTERED',
|
||||||
|
resource: {
|
||||||
|
values: [vendasHeaders]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Formatar cabeçalhos (opcional, pode falhar sem quebrar)
|
||||||
|
try {
|
||||||
|
const requests = [
|
||||||
|
{
|
||||||
|
repeatCell: {
|
||||||
|
range: {
|
||||||
|
sheetId: 0,
|
||||||
|
startRowIndex: 0,
|
||||||
|
endRowIndex: 1,
|
||||||
|
startColumnIndex: 0,
|
||||||
|
endColumnIndex: produtosHeaders.length
|
||||||
|
},
|
||||||
|
cell: {
|
||||||
|
userEnteredFormat: {
|
||||||
|
backgroundColor: { red: 0.2, green: 0.6, blue: 1.0 },
|
||||||
|
textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fields: 'userEnteredFormat'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
repeatCell: {
|
||||||
|
range: {
|
||||||
|
sheetId: 1,
|
||||||
|
startRowIndex: 0,
|
||||||
|
endRowIndex: 1,
|
||||||
|
startColumnIndex: 0,
|
||||||
|
endColumnIndex: vendasHeaders.length
|
||||||
|
},
|
||||||
|
cell: {
|
||||||
|
userEnteredFormat: {
|
||||||
|
backgroundColor: { red: 0.2, green: 0.8, blue: 0.2 },
|
||||||
|
textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fields: 'userEnteredFormat'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await this.sheets.spreadsheets.batchUpdate({
|
||||||
|
spreadsheetId,
|
||||||
|
resource: { requests }
|
||||||
|
});
|
||||||
|
} catch (formatError) {
|
||||||
|
console.log('Aviso: Não foi possível formatar cabeçalhos, mas planilha foi criada:', formatError.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao configurar cabeçalhos:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exportar dados de produtos
|
||||||
|
async exportProdutos(spreadsheetId, produtos) {
|
||||||
|
try {
|
||||||
|
const values = produtos.map(produto => [
|
||||||
|
produto.id || '',
|
||||||
|
produto.nome || '',
|
||||||
|
produto.fornecedor_nome || '',
|
||||||
|
produto.tamanho || '',
|
||||||
|
produto.estacao || '',
|
||||||
|
produto.genero || '',
|
||||||
|
produto.valor_compra || 0,
|
||||||
|
produto.valor_revenda || 0,
|
||||||
|
produto.created_at ? new Date(produto.created_at).toLocaleDateString('pt-BR') : '',
|
||||||
|
'', // Data da venda será preenchida quando houver venda
|
||||||
|
produto.quantidade_total || 0,
|
||||||
|
produto.marca || ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
await this.sheets.spreadsheets.values.update({
|
||||||
|
spreadsheetId,
|
||||||
|
range: 'Produtos!A2:L' + (values.length + 1),
|
||||||
|
valueInputOption: 'USER_ENTERED',
|
||||||
|
resource: { values }
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, rowsUpdated: values.length };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao exportar produtos:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exportar dados de vendas
|
||||||
|
async exportVendas(spreadsheetId, vendas) {
|
||||||
|
try {
|
||||||
|
const values = vendas.map(venda => [
|
||||||
|
venda.id || '',
|
||||||
|
venda.cliente_nome || 'Cliente não informado',
|
||||||
|
venda.data_venda ? new Date(venda.data_venda).toLocaleDateString('pt-BR') : '',
|
||||||
|
venda.tipo_pagamento || '',
|
||||||
|
venda.valor_total || 0,
|
||||||
|
venda.desconto || 0,
|
||||||
|
venda.valor_final || 0,
|
||||||
|
venda.status || 'Concluída',
|
||||||
|
venda.observacoes || '',
|
||||||
|
venda.produtos ? venda.produtos.map(p => `${p.nome} (${p.quantidade}x)`).join(', ') : ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
await this.sheets.spreadsheets.values.update({
|
||||||
|
spreadsheetId,
|
||||||
|
range: 'Vendas!A2:J' + (values.length + 1),
|
||||||
|
valueInputOption: 'USER_ENTERED',
|
||||||
|
resource: { values }
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, rowsUpdated: values.length };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao exportar vendas:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se está autenticado
|
||||||
|
isAuthenticated() {
|
||||||
|
return this.auth && this.auth.credentials && this.auth.credentials.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se o token está próximo do vencimento
|
||||||
|
isTokenExpiringSoon() {
|
||||||
|
if (!this.auth || !this.auth.credentials || !this.auth.credentials.expiry_date) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const expiry = this.auth.credentials.expiry_date;
|
||||||
|
const timeUntilExpiry = expiry - now;
|
||||||
|
|
||||||
|
// Considera que está expirando se faltam menos de 5 minutos
|
||||||
|
return timeUntilExpiry < 5 * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renovar token automaticamente se necessário
|
||||||
|
async refreshTokenIfNeeded() {
|
||||||
|
if (this.isTokenExpiringSoon()) {
|
||||||
|
try {
|
||||||
|
const { credentials } = await this.auth.refreshAccessToken();
|
||||||
|
this.auth.setCredentials(credentials);
|
||||||
|
|
||||||
|
// Salvar tokens atualizados
|
||||||
|
const tokensPath = path.join(__dirname, 'google-tokens.json');
|
||||||
|
fs.writeFileSync(tokensPath, JSON.stringify(credentials, null, 2));
|
||||||
|
|
||||||
|
console.log('Token Google renovado automaticamente');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao renovar token Google:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resetar o serviço (limpar autenticação)
|
||||||
|
reset() {
|
||||||
|
this.auth = null;
|
||||||
|
this.sheets = null;
|
||||||
|
this.drive = null;
|
||||||
|
console.log('Serviço Google Sheets resetado');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new GoogleSheetsService();
|
||||||
7
config/google-tokens.json
Normal file
7
config/google-tokens.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"access_token": "ya29.a0AQQ_BDTf90ixT9y_x6M_zK1qmBhM88FscZ9xmiUDv0acKlSDkge5lxiUEYIi3yxFFiwxv3Az9P-PnV1wWnfEscTcht6z5uvp3LT0uzS3uFWndQ3HWN3X-YVxnvpyBD7P3gCc6YGHz0zHeN1QtyXTAIM9iW0hwLf8R5d88b_EI2DftLLZ7F68jo_MOQwN9pwUkmb6DWlaaCgYKAVoSARcSFQHGX2Milg4J_31KZx9930lNX7B8Hg0207",
|
||||||
|
"scope": "https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/spreadsheets",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expiry_date": 1759955985106,
|
||||||
|
"refresh_token": "1//0hZ-EhpsmoFVGCgYIARAAGBESNwF-L9IrIlcr7zua7Q-wDP3Eg6-hje3BoKCDYdaMVWJMqa3VGDx57VgI0O40iu5hYtsbIsg_bTc"
|
||||||
|
}
|
||||||
82
config/mercadopago-demo.js
Normal file
82
config/mercadopago-demo.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Versão DEMO do Mercado Pago para desenvolvimento
|
||||||
|
// Simula a geração de PIX sem credenciais reais
|
||||||
|
|
||||||
|
class MercadoPagoServiceDemo {
|
||||||
|
constructor() {
|
||||||
|
console.log('🎭 Modo DEMO: Simulando Mercado Pago para desenvolvimento');
|
||||||
|
}
|
||||||
|
|
||||||
|
async gerarPix(dados) {
|
||||||
|
try {
|
||||||
|
console.log('🏦 [DEMO] Gerando PIX com dados:', dados);
|
||||||
|
|
||||||
|
// Simular delay da API
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Gerar dados fictícios mas realistas
|
||||||
|
const payment_id = `demo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const qr_code = this.gerarQRCodeDemo();
|
||||||
|
const qr_code_base64 = this.gerarQRCodeBase64Demo();
|
||||||
|
|
||||||
|
console.log('✅ [DEMO] PIX gerado com sucesso!');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payment_id: payment_id,
|
||||||
|
qr_code: qr_code,
|
||||||
|
qr_code_base64: qr_code_base64,
|
||||||
|
pix_copy_paste: qr_code,
|
||||||
|
expiration_date: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
|
||||||
|
transaction_amount: parseFloat(dados.valor),
|
||||||
|
status: 'pending'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [DEMO] Erro ao gerar PIX:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async consultarPagamento(payment_id) {
|
||||||
|
try {
|
||||||
|
console.log('🔍 [DEMO] Consultando pagamento:', payment_id);
|
||||||
|
|
||||||
|
// Simular delay da API
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// Para demo, sempre retorna pendente
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
status: 'pending',
|
||||||
|
status_detail: 'pending_waiting_payment',
|
||||||
|
transaction_amount: 10.00,
|
||||||
|
date_approved: null,
|
||||||
|
external_reference: payment_id.replace('demo_', '')
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [DEMO] Erro ao consultar pagamento:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gerarQRCodeDemo() {
|
||||||
|
// QR Code PIX fictício mas com formato real
|
||||||
|
const timestamp = Date.now().toString();
|
||||||
|
const random = Math.random().toString(36).substr(2, 10);
|
||||||
|
|
||||||
|
return `00020126580014br.gov.bcb.pix0136${random}5204000053039865802BR5925LIBERI KIDS DEMO STORE6009SAO PAULO62070503***6304${timestamp.substr(-4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
gerarQRCodeBase64Demo() {
|
||||||
|
// QR Code base64 fictício (imagem 1x1 pixel transparente)
|
||||||
|
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new MercadoPagoServiceDemo();
|
||||||
81
config/mercadopago.js
Normal file
81
config/mercadopago.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
const { MercadoPagoConfig, Payment } = require('mercadopago');
|
||||||
|
|
||||||
|
class MercadoPagoService {
|
||||||
|
constructor() {
|
||||||
|
if (process.env.MERCADOPAGO_ACCESS_TOKEN) {
|
||||||
|
this.client = new MercadoPagoConfig({
|
||||||
|
accessToken: process.env.MERCADOPAGO_ACCESS_TOKEN,
|
||||||
|
options: { timeout: 5000 }
|
||||||
|
});
|
||||||
|
this.payment = new Payment(this.client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async gerarPix(dados) {
|
||||||
|
try {
|
||||||
|
console.log('🏦 Gerando PIX com dados:', dados);
|
||||||
|
console.log('🔑 Access Token:', process.env.MERCADOPAGO_ACCESS_TOKEN ? 'Configurado' : 'NÃO CONFIGURADO');
|
||||||
|
|
||||||
|
const payment_data = {
|
||||||
|
transaction_amount: parseFloat(dados.valor),
|
||||||
|
description: dados.descricao || `Venda #${dados.venda_id} - Liberi Kids`,
|
||||||
|
payment_method_id: 'pix',
|
||||||
|
payer: {
|
||||||
|
email: dados.cliente_email || 'cliente@liberikids.com',
|
||||||
|
first_name: dados.cliente_nome || 'Cliente',
|
||||||
|
identification: {
|
||||||
|
type: 'CPF',
|
||||||
|
number: dados.cliente_cpf || '00000000000'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
external_reference: dados.venda_id.toString(),
|
||||||
|
// notification_url removida para desenvolvimento local
|
||||||
|
date_of_expiration: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30 minutos
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📤 Enviando dados para Mercado Pago:', JSON.stringify(payment_data, null, 2));
|
||||||
|
const payment = await this.payment.create({ body: payment_data });
|
||||||
|
console.log('✅ Resposta do Mercado Pago:', payment);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payment_id: payment.id,
|
||||||
|
qr_code: payment.point_of_interaction.transaction_data.qr_code,
|
||||||
|
qr_code_base64: payment.point_of_interaction.transaction_data.qr_code_base64,
|
||||||
|
pix_copy_paste: payment.point_of_interaction.transaction_data.qr_code,
|
||||||
|
expiration_date: payment.date_of_expiration,
|
||||||
|
transaction_amount: payment.transaction_amount,
|
||||||
|
status: payment.status
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao gerar PIX:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async consultarPagamento(payment_id) {
|
||||||
|
try {
|
||||||
|
const payment = await this.payment.get({ id: payment_id });
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
status: payment.status,
|
||||||
|
status_detail: payment.status_detail,
|
||||||
|
transaction_amount: payment.transaction_amount,
|
||||||
|
date_approved: payment.date_approved,
|
||||||
|
external_reference: payment.external_reference
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao consultar pagamento:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new MercadoPagoService();
|
||||||
8
config/supabase.js
Normal file
8
config/supabase.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
const { createClient } = require('@supabase/supabase-js');
|
||||||
|
|
||||||
|
const supabaseUrl = 'https://xyqmlesqdqybiyjofysb.supabase.co';
|
||||||
|
const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh5cW1sZXNxZHF5Yml5am9meXNiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk2NjEzMzcsImV4cCI6MjA3NTIzNzMzN30.uXPONkstd_xXbzX1ZwlB9gK05zjwQL0Ymj94_3NnOGE';
|
||||||
|
|
||||||
|
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||||
|
|
||||||
|
module.exports = supabase;
|
||||||
129
configurar-env-local.sh
Executable file
129
configurar-env-local.sh
Executable file
@@ -0,0 +1,129 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🔧 Script para Configurar .env Local Automaticamente
|
||||||
|
# Execute: ./configurar-env-local.sh
|
||||||
|
|
||||||
|
echo "🔧 Configurando .env Local - Liberi Kids"
|
||||||
|
echo "======================================="
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[✅ OK]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[⚠️ WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo -e "${BLUE}[ℹ️ INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[❌ ERRO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verificar se .env existe
|
||||||
|
if [ ! -f ".env" ]; then
|
||||||
|
error "Arquivo .env não encontrado!"
|
||||||
|
info "Criando .env a partir do .env.example..."
|
||||||
|
cp .env.example .env
|
||||||
|
log ".env criado com sucesso"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "📝 CONFIGURANDO CREDENCIAIS..."
|
||||||
|
|
||||||
|
# Solicitar credenciais do Supabase
|
||||||
|
echo ""
|
||||||
|
info "🗄️ CONFIGURAÇÕES SUPABASE:"
|
||||||
|
echo "Acesse: https://supabase.com/dashboard/project/SEU_PROJETO/settings/api"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "Digite a URL do Supabase (https://seu-projeto.supabase.co): " SUPABASE_URL
|
||||||
|
read -p "Digite a ANON KEY do Supabase: " SUPABASE_ANON_KEY
|
||||||
|
|
||||||
|
# Solicitar credenciais do Mercado Pago (opcional)
|
||||||
|
echo ""
|
||||||
|
info "🏦 CONFIGURAÇÕES PIX - MERCADO PAGO (Opcional):"
|
||||||
|
echo "Acesse: https://www.mercadopago.com.br/developers"
|
||||||
|
echo "Deixe em branco para pular esta configuração"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "Digite o ACCESS TOKEN (TEST-...): " MERCADO_PAGO_TOKEN
|
||||||
|
read -p "Digite a PUBLIC KEY (pk_test_...): " MERCADO_PAGO_PUBLIC_KEY
|
||||||
|
|
||||||
|
# Aplicar configurações no .env
|
||||||
|
echo ""
|
||||||
|
info "⚙️ APLICANDO CONFIGURAÇÕES..."
|
||||||
|
|
||||||
|
# Configurar Supabase
|
||||||
|
if [ ! -z "$SUPABASE_URL" ]; then
|
||||||
|
sed -i "s|SUPABASE_URL=.*|SUPABASE_URL=$SUPABASE_URL|g" .env
|
||||||
|
log "URL Supabase configurada"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$SUPABASE_ANON_KEY" ]; then
|
||||||
|
sed -i "s|SUPABASE_ANON_KEY=.*|SUPABASE_ANON_KEY=$SUPABASE_ANON_KEY|g" .env
|
||||||
|
log "ANON KEY Supabase configurada"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configurar Mercado Pago (se fornecido)
|
||||||
|
if [ ! -z "$MERCADO_PAGO_TOKEN" ]; then
|
||||||
|
sed -i "s|MERCADOPAGO_ACCESS_TOKEN=.*|MERCADOPAGO_ACCESS_TOKEN=$MERCADO_PAGO_TOKEN|g" .env
|
||||||
|
log "Access Token Mercado Pago configurado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$MERCADO_PAGO_PUBLIC_KEY" ]; then
|
||||||
|
sed -i "s|MERCADOPAGO_PUBLIC_KEY=.*|MERCADOPAGO_PUBLIC_KEY=$MERCADO_PAGO_PUBLIC_KEY|g" .env
|
||||||
|
log "Public Key Mercado Pago configurada"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configurar ambiente local
|
||||||
|
sed -i "s|NODE_ENV=production|NODE_ENV=development|g" .env
|
||||||
|
sed -i "s|BASE_URL=http://localhost:5000|BASE_URL=http://localhost:5000|g" .env
|
||||||
|
|
||||||
|
log "Ambiente configurado para desenvolvimento local"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "🎉 CONFIGURAÇÃO CONCLUÍDA!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "📋 PRÓXIMOS PASSOS:"
|
||||||
|
echo ""
|
||||||
|
echo "1. 🗄️ APLICAR SQL NO SUPABASE:"
|
||||||
|
echo " - Acesse: https://supabase.com/dashboard"
|
||||||
|
echo " - Vá em SQL Editor"
|
||||||
|
echo " - Execute o conteúdo de: aplicar-pix-supabase.sql"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "2. 🚀 EXECUTAR SERVIDOR LOCAL:"
|
||||||
|
echo " node server-supabase.js"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "3. 🌐 ACESSAR SISTEMA:"
|
||||||
|
echo " http://localhost:5000"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "4. 🧪 TESTAR PIX:"
|
||||||
|
echo " - Vá em Vendas"
|
||||||
|
echo " - Clique no botão PIX (💳)"
|
||||||
|
echo " - Verifique se QR Code é gerado"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -z "$MERCADO_PAGO_TOKEN" ]; then
|
||||||
|
warn "⚠️ PIX não configurado - configure depois se necessário"
|
||||||
|
echo " Para configurar PIX:"
|
||||||
|
echo " 1. Acesse: https://www.mercadopago.com.br/developers"
|
||||||
|
echo " 2. Crie conta e aplicação"
|
||||||
|
echo " 3. Execute este script novamente"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "✨ SEU AMBIENTE LOCAL ESTÁ PRONTO!"
|
||||||
157
configurar-pix-servidor.sh
Executable file
157
configurar-pix-servidor.sh
Executable file
@@ -0,0 +1,157 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🏦 Script para Configurar PIX no Servidor
|
||||||
|
# Execute no servidor: ./configurar-pix-servidor.sh
|
||||||
|
|
||||||
|
echo "🏦 Configurando PIX no Servidor - Liberi Kids"
|
||||||
|
echo "============================================"
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verificar se estamos no diretório correto
|
||||||
|
if [ ! -f "server-supabase.js" ]; then
|
||||||
|
error "Arquivo server-supabase.js não encontrado!"
|
||||||
|
echo "Execute este script no diretório raiz do projeto (onde está o server-supabase.js)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "📁 Diretório correto encontrado"
|
||||||
|
|
||||||
|
# Verificar se .env existe
|
||||||
|
if [ ! -f ".env" ]; then
|
||||||
|
warn "⚠️ Arquivo .env não encontrado!"
|
||||||
|
echo "Criando .env a partir do .env.example..."
|
||||||
|
|
||||||
|
if [ -f ".env.example" ]; then
|
||||||
|
cp .env.example .env
|
||||||
|
log "✅ Arquivo .env criado"
|
||||||
|
else
|
||||||
|
error "❌ Arquivo .env.example não encontrado!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar se as credenciais PIX estão no .env
|
||||||
|
if ! grep -q "MERCADOPAGO_ACCESS_TOKEN" .env; then
|
||||||
|
warn "⚠️ Credenciais PIX não encontradas no .env"
|
||||||
|
echo ""
|
||||||
|
echo "Adicionando configurações PIX ao .env..."
|
||||||
|
|
||||||
|
cat >> .env << 'EOF'
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# CONFIGURAÇÕES PIX - MERCADO PAGO
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Access Token do Mercado Pago (TEST para desenvolvimento, PROD para produção)
|
||||||
|
MERCADOPAGO_ACCESS_TOKEN=TEST-sua_access_token_aqui
|
||||||
|
|
||||||
|
# Public Key do Mercado Pago
|
||||||
|
MERCADOPAGO_PUBLIC_KEY=pk_test_sua_public_key_aqui
|
||||||
|
|
||||||
|
# URL base para webhooks (importante para produção)
|
||||||
|
BASE_URL=http://localhost:5000
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "✅ Configurações PIX adicionadas ao .env"
|
||||||
|
warn "📝 EDITE O ARQUIVO .env E CONFIGURE SUAS CREDENCIAIS!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar se node_modules existe
|
||||||
|
if [ ! -d "node_modules" ]; then
|
||||||
|
warn "⚠️ node_modules não encontrado. Instalando dependências..."
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar se mercadopago está instalado
|
||||||
|
if [ ! -d "node_modules/mercadopago" ]; then
|
||||||
|
info "📦 Instalando SDK do Mercado Pago..."
|
||||||
|
npm install mercadopago
|
||||||
|
log "✅ Mercado Pago SDK instalado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar se o build do frontend existe
|
||||||
|
if [ ! -d "client/build" ]; then
|
||||||
|
warn "⚠️ Build do frontend não encontrado"
|
||||||
|
info "🔨 Fazendo build do frontend..."
|
||||||
|
cd client
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
log "✅ Build do frontend concluído"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🎯 CONFIGURAÇÃO PIX FINALIZADA!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "✅ Arquivos PIX criados:"
|
||||||
|
echo " - config/mercadopago.js (Serviço PIX)"
|
||||||
|
echo " - client/src/styles/pix-integration.css (Estilos)"
|
||||||
|
echo " - aplicar-pix-supabase.sql (SQL para Supabase)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
warn "📝 PRÓXIMOS PASSOS OBRIGATÓRIOS:"
|
||||||
|
echo ""
|
||||||
|
echo "1. 🏦 CRIAR CONTA MERCADO PAGO:"
|
||||||
|
echo " - Acesse: https://www.mercadopago.com.br/developers"
|
||||||
|
echo " - Crie conta e aplicação de 'Pagamentos online'"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "2. 📝 CONFIGURAR CREDENCIAIS:"
|
||||||
|
echo " - Edite o arquivo .env:"
|
||||||
|
echo " nano .env"
|
||||||
|
echo " - Configure:"
|
||||||
|
echo " MERCADOPAGO_ACCESS_TOKEN=TEST-sua_access_token"
|
||||||
|
echo " MERCADOPAGO_PUBLIC_KEY=pk_test_sua_public_key"
|
||||||
|
echo " BASE_URL=http://$(hostname -I | awk '{print $1}'):5000"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "3. 🗄️ APLICAR SQL NO SUPABASE:"
|
||||||
|
echo " - Acesse o painel do Supabase"
|
||||||
|
echo " - Vá em SQL Editor"
|
||||||
|
echo " - Execute o conteúdo do arquivo: aplicar-pix-supabase.sql"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "4. 🔄 REINICIAR SERVIDOR:"
|
||||||
|
echo " - pm2 restart liberi-kids"
|
||||||
|
echo " - Ou: pm2 start server-supabase.js --name liberi-kids"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "5. 🧪 TESTAR:"
|
||||||
|
echo " - Acesse o sistema"
|
||||||
|
echo " - Vá em Vendas"
|
||||||
|
echo " - Clique no botão PIX (💳)"
|
||||||
|
echo " - Verifique se QR Code é gerado"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "💡 DICAS:"
|
||||||
|
echo " - Use credenciais TEST para desenvolvimento"
|
||||||
|
echo " - Configure webhook URL no Mercado Pago"
|
||||||
|
echo " - Teste pagamentos no ambiente sandbox"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "🎉 PIX PRONTO PARA CONFIGURAÇÃO!"
|
||||||
|
echo ""
|
||||||
|
warn "Configure as credenciais e execute o SQL no Supabase!"
|
||||||
158
configurar-producao-pix.sh
Executable file
158
configurar-producao-pix.sh
Executable file
@@ -0,0 +1,158 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🏦 Script para Configurar PIX Produção - Mercado Pago
|
||||||
|
# Execute: ./configurar-producao-pix.sh
|
||||||
|
|
||||||
|
echo "🏦 Configuração PIX Produção - Liberi Kids"
|
||||||
|
echo "========================================="
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[✅ OK]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[⚠️ WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo -e "${BLUE}[ℹ️ INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[❌ ERRO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
warn "⚠️ ATENÇÃO: Este script configura PIX para PRODUÇÃO"
|
||||||
|
warn "⚠️ Certifique-se de ter credenciais VÁLIDAS do Mercado Pago"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "📋 REQUISITOS OBRIGATÓRIOS:"
|
||||||
|
echo " ✅ Conta Mercado Pago EMPRESARIAL (não pessoal)"
|
||||||
|
echo " ✅ CNPJ verificado"
|
||||||
|
echo " ✅ Conta bancária confirmada"
|
||||||
|
echo " ✅ PIX ativado na conta"
|
||||||
|
echo " ✅ Aplicação criada no painel de desenvolvedores"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "Você tem TODOS os requisitos acima? (s/N): " confirma
|
||||||
|
if [[ $confirma != "s" && $confirma != "S" ]]; then
|
||||||
|
error "❌ Configure os requisitos primeiro!"
|
||||||
|
echo ""
|
||||||
|
info "📖 Guia completo:"
|
||||||
|
echo " 1. Acesse: https://www.mercadopago.com.br/developers"
|
||||||
|
echo " 2. Crie conta empresarial com CNPJ"
|
||||||
|
echo " 3. Complete verificação de identidade"
|
||||||
|
echo " 4. Ative PIX na conta"
|
||||||
|
echo " 5. Crie aplicação 'Checkout Pro'"
|
||||||
|
echo " 6. Execute este script novamente"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🔑 CONFIGURANDO CREDENCIAIS DE PRODUÇÃO..."
|
||||||
|
|
||||||
|
# Solicitar credenciais
|
||||||
|
echo ""
|
||||||
|
info "📝 Digite suas credenciais do Mercado Pago:"
|
||||||
|
echo "Encontre em: https://www.mercadopago.com.br/developers/panel/app"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "Access Token (APP-...): " ACCESS_TOKEN
|
||||||
|
read -p "Public Key (pk_live_...): " PUBLIC_KEY
|
||||||
|
read -p "URL do seu servidor (https://seudominio.com): " BASE_URL
|
||||||
|
|
||||||
|
# Validar credenciais
|
||||||
|
if [[ ! $ACCESS_TOKEN =~ ^APP- ]] && [[ ! $ACCESS_TOKEN =~ ^APP_USR- ]]; then
|
||||||
|
error "❌ Access Token deve começar com 'APP-' ou 'APP_USR-' para produção"
|
||||||
|
warn "💡 Para teste, use 'TEST-...'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! $PUBLIC_KEY =~ ^pk_live_ ]] && [[ ! $PUBLIC_KEY =~ ^APP_USR- ]]; then
|
||||||
|
error "❌ Public Key deve começar com 'pk_live_' ou 'APP_USR-' para produção"
|
||||||
|
warn "💡 Para teste, use 'pk_test_...'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! $BASE_URL =~ ^https:// ]]; then
|
||||||
|
error "❌ URL deve usar HTTPS para produção"
|
||||||
|
warn "💡 Para desenvolvimento local, use 'http://localhost:5000'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar se .env existe
|
||||||
|
if [ ! -f ".env" ]; then
|
||||||
|
info "📝 Criando arquivo .env..."
|
||||||
|
cp .env.example .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aplicar configurações
|
||||||
|
info "⚙️ APLICANDO CREDENCIAIS..."
|
||||||
|
|
||||||
|
sed -i "s|MERCADOPAGO_ACCESS_TOKEN=.*|MERCADOPAGO_ACCESS_TOKEN=$ACCESS_TOKEN|g" .env
|
||||||
|
sed -i "s|MERCADOPAGO_PUBLIC_KEY=.*|MERCADOPAGO_PUBLIC_KEY=$PUBLIC_KEY|g" .env
|
||||||
|
sed -i "s|BASE_URL=.*|BASE_URL=$BASE_URL|g" .env
|
||||||
|
sed -i "s|NODE_ENV=.*|NODE_ENV=production|g" .env
|
||||||
|
|
||||||
|
log "✅ Credenciais configuradas"
|
||||||
|
|
||||||
|
# Substituir versão demo por produção
|
||||||
|
info "🔄 ATIVANDO VERSÃO DE PRODUÇÃO..."
|
||||||
|
|
||||||
|
if [ -f "server-supabase.js" ]; then
|
||||||
|
# Trocar mercadopago-demo por mercadopago
|
||||||
|
sed -i "s|require('./config/mercadopago-demo')|require('./config/mercadopago')|g" server-supabase.js
|
||||||
|
sed -i "s|// Usar versão demo para desenvolvimento|// Versão de produção ativada|g" server-supabase.js
|
||||||
|
log "✅ Servidor configurado para produção"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "🎉 CONFIGURAÇÃO DE PRODUÇÃO CONCLUÍDA!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "📋 PRÓXIMOS PASSOS:"
|
||||||
|
echo ""
|
||||||
|
echo "1. 🧪 TESTAR EM AMBIENTE SEGURO:"
|
||||||
|
echo " - Faça um PIX de R$ 0,01 primeiro"
|
||||||
|
echo " - Verifique se recebe na conta"
|
||||||
|
echo " - Confirme se webhook funciona"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "2. 🌐 CONFIGURAR WEBHOOK NO MERCADO PAGO:"
|
||||||
|
echo " - Acesse: https://www.mercadopago.com.br/developers"
|
||||||
|
echo " - Vá em sua aplicação → Webhooks"
|
||||||
|
echo " - Configure: $BASE_URL/api/pix/webhook"
|
||||||
|
echo " - Eventos: payment"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "3. 🚀 REINICIAR SERVIDOR:"
|
||||||
|
if command -v pm2 &> /dev/null; then
|
||||||
|
echo " pm2 restart liberi-kids"
|
||||||
|
else
|
||||||
|
echo " node server-supabase.js"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "4. 🔒 SEGURANÇA:"
|
||||||
|
echo " - Use HTTPS obrigatoriamente"
|
||||||
|
echo " - Mantenha .env seguro"
|
||||||
|
echo " - Monitore transações"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
warn "⚠️ IMPORTANTE:"
|
||||||
|
echo " - PIX de produção cobra 0,99% por transação"
|
||||||
|
echo " - Recebimento em 1 dia útil"
|
||||||
|
echo " - Teste com valores baixos primeiro"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "🏦 SEU PIX DE PRODUÇÃO ESTÁ CONFIGURADO!"
|
||||||
|
echo ""
|
||||||
|
info "💡 Para voltar ao modo demo: ./configurar-env-local.sh"
|
||||||
173
deploy-completo-servidor.sh
Executable file
173
deploy-completo-servidor.sh
Executable file
@@ -0,0 +1,173 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 Deploy COMPLETO: PC → Servidor Limpo
|
||||||
|
# Este script envia TUDO do seu PC para o servidor
|
||||||
|
# Execute: ./deploy-completo-servidor.sh
|
||||||
|
|
||||||
|
echo "🚀 DEPLOY COMPLETO - Liberi Kids"
|
||||||
|
echo "================================"
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[✅ OK]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[⚠️ WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo -e "${BLUE}[ℹ️ INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[❌ ERRO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configurações do servidor
|
||||||
|
SERVER_IP="192.168.195.145"
|
||||||
|
SERVER_USER="tiago"
|
||||||
|
SERVER_PATH="/home/tiago/app_estoque"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "📋 ESTE SCRIPT VAI ENVIAR:"
|
||||||
|
echo " 📁 Todos os arquivos do projeto"
|
||||||
|
echo " 🔧 Configurações e credenciais"
|
||||||
|
echo " 📦 Frontend compilado"
|
||||||
|
echo " 🏦 Configurações PIX"
|
||||||
|
echo " 🗄️ Scripts SQL"
|
||||||
|
echo " 📚 Documentação completa"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "Continuar com o deploy completo? (s/N): " confirma
|
||||||
|
if [[ $confirma != "s" && $confirma != "S" ]]; then
|
||||||
|
error "❌ Deploy cancelado!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🔨 PREPARANDO DEPLOY..."
|
||||||
|
|
||||||
|
# Verificar se o build existe
|
||||||
|
if [ ! -d "client/build" ]; then
|
||||||
|
info "🏗️ Compilando frontend..."
|
||||||
|
cd client
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
log "✅ Frontend compilado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Criar diretório temporário para deploy
|
||||||
|
TEMP_DIR="/tmp/liberi-deploy-$(date +%s)"
|
||||||
|
mkdir -p $TEMP_DIR
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "📦 CRIANDO PACOTE DE DEPLOY..."
|
||||||
|
|
||||||
|
# Copiar arquivos essenciais
|
||||||
|
cp -r client/build $TEMP_DIR/
|
||||||
|
cp server-supabase.js $TEMP_DIR/
|
||||||
|
cp package.json $TEMP_DIR/
|
||||||
|
cp -r config $TEMP_DIR/
|
||||||
|
cp .env $TEMP_DIR/ 2>/dev/null || cp .env.example $TEMP_DIR/.env
|
||||||
|
|
||||||
|
# Copiar scripts e documentação
|
||||||
|
cp *.sh $TEMP_DIR/ 2>/dev/null || true
|
||||||
|
cp *.md $TEMP_DIR/ 2>/dev/null || true
|
||||||
|
cp *.sql $TEMP_DIR/ 2>/dev/null || true
|
||||||
|
|
||||||
|
# Criar estrutura de diretórios
|
||||||
|
mkdir -p $TEMP_DIR/uploads
|
||||||
|
mkdir -p $TEMP_DIR/logs
|
||||||
|
|
||||||
|
log "✅ Pacote criado: $TEMP_DIR"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🌐 ENVIANDO PARA SERVIDOR..."
|
||||||
|
|
||||||
|
# Enviar tudo via rsync
|
||||||
|
rsync -avz --progress $TEMP_DIR/ $SERVER_USER@$SERVER_IP:$SERVER_PATH/
|
||||||
|
|
||||||
|
log "✅ Arquivos enviados"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "📦 INSTALANDO DEPENDÊNCIAS NO SERVIDOR..."
|
||||||
|
|
||||||
|
# Instalar dependências
|
||||||
|
ssh $SERVER_USER@$SERVER_IP "cd $SERVER_PATH && npm install"
|
||||||
|
|
||||||
|
log "✅ Dependências instaladas"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🔧 CONFIGURANDO SERVIDOR..."
|
||||||
|
|
||||||
|
# Configurar permissões
|
||||||
|
ssh $SERVER_USER@$SERVER_IP "cd $SERVER_PATH && chmod +x *.sh"
|
||||||
|
|
||||||
|
# Verificar se .env existe
|
||||||
|
ssh $SERVER_USER@$SERVER_IP "cd $SERVER_PATH && [ ! -f .env ] && cp .env.example .env || true"
|
||||||
|
|
||||||
|
log "✅ Servidor configurado"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🚀 INICIANDO APLICAÇÃO..."
|
||||||
|
|
||||||
|
# Iniciar com PM2
|
||||||
|
ssh $SERVER_USER@$SERVER_IP "cd $SERVER_PATH && pm2 start server-supabase.js --name liberi-kids"
|
||||||
|
ssh $SERVER_USER@$SERVER_IP "pm2 save"
|
||||||
|
|
||||||
|
log "✅ Aplicação iniciada"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🧪 TESTANDO SERVIDOR..."
|
||||||
|
|
||||||
|
# Aguardar inicialização
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Testar se está respondendo
|
||||||
|
if ssh $SERVER_USER@$SERVER_IP "curl -s http://localhost:5000 > /dev/null"; then
|
||||||
|
log "✅ Servidor respondendo"
|
||||||
|
else
|
||||||
|
warn "⚠️ Servidor pode não estar respondendo"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Limpar arquivos temporários
|
||||||
|
rm -rf $TEMP_DIR
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "🎉 DEPLOY COMPLETO FINALIZADO!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "📋 RESUMO DO DEPLOY:"
|
||||||
|
echo " ✅ Arquivos enviados: $(ssh $SERVER_USER@$SERVER_IP "cd $SERVER_PATH && find . -type f | wc -l") arquivos"
|
||||||
|
echo " ✅ Dependências: Instaladas"
|
||||||
|
echo " ✅ PM2: Configurado"
|
||||||
|
echo " ✅ Servidor: Rodando"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "🌐 ACESSO:"
|
||||||
|
echo " Frontend: http://$SERVER_IP:5000"
|
||||||
|
echo " Status: pm2 status"
|
||||||
|
echo " Logs: pm2 logs liberi-kids"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "🔧 COMANDOS ÚTEIS NO SERVIDOR:"
|
||||||
|
echo " ssh $SERVER_USER@$SERVER_IP"
|
||||||
|
echo " cd $SERVER_PATH"
|
||||||
|
echo " pm2 restart liberi-kids"
|
||||||
|
echo " pm2 logs liberi-kids"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
warn "💡 LEMBRE-SE:"
|
||||||
|
echo " - Configure credenciais no .env se necessário"
|
||||||
|
echo " - Aplique SQL no Supabase se for primeira vez"
|
||||||
|
echo " - Teste todas as funcionalidades"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "✨ SEU PROJETO ESTÁ NO AR!"
|
||||||
221
deploy-pix-completo.sh
Executable file
221
deploy-pix-completo.sh
Executable file
@@ -0,0 +1,221 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 Deploy PIX Completo - Local para Servidor
|
||||||
|
# Execute: ./deploy-pix-completo.sh usuario@servidor
|
||||||
|
|
||||||
|
echo "🚀 Deploy PIX - Liberi Kids (Local → Servidor)"
|
||||||
|
echo "=============================================="
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verificar parâmetros
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
error "❌ Uso: ./deploy-pix-completo.sh usuario@servidor"
|
||||||
|
echo "Exemplo: ./deploy-pix-completo.sh tiago@192.168.195.145"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVIDOR=$1
|
||||||
|
info "🎯 Servidor destino: $SERVIDOR"
|
||||||
|
|
||||||
|
# Verificar se estamos no diretório correto
|
||||||
|
if [ ! -f "server-supabase.js" ]; then
|
||||||
|
error "❌ Execute este script no diretório raiz do projeto!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "📦 ETAPA 1: Preparando arquivos locais..."
|
||||||
|
|
||||||
|
# Build do frontend
|
||||||
|
if [ -d "client" ]; then
|
||||||
|
info "🔨 Fazendo build do frontend..."
|
||||||
|
cd client
|
||||||
|
npm run build
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "✅ Build do frontend concluído"
|
||||||
|
else
|
||||||
|
error "❌ Erro no build do frontend"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cd ..
|
||||||
|
else
|
||||||
|
warn "⚠️ Diretório client não encontrado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "📤 ETAPA 2: Enviando arquivos para o servidor..."
|
||||||
|
|
||||||
|
# Criar lista de arquivos para enviar
|
||||||
|
ARQUIVOS_ENVIAR=(
|
||||||
|
"server-supabase.js"
|
||||||
|
"config/"
|
||||||
|
"sql/"
|
||||||
|
"scripts/"
|
||||||
|
"client/build/"
|
||||||
|
"package.json"
|
||||||
|
".env.example"
|
||||||
|
"aplicar-pix-supabase.sql"
|
||||||
|
"configurar-pix-servidor.sh"
|
||||||
|
"INTEGRACAO-PIX-GUIDE.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verificar conectividade SSH
|
||||||
|
info "🔗 Testando conexão SSH..."
|
||||||
|
ssh -o ConnectTimeout=10 -o BatchMode=yes $SERVIDOR exit
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error "❌ Não foi possível conectar ao servidor $SERVIDOR"
|
||||||
|
echo "Verifique:"
|
||||||
|
echo " - Se o servidor está ligado"
|
||||||
|
echo " - Se o SSH está funcionando"
|
||||||
|
echo " - Se as credenciais estão corretas"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "✅ Conexão SSH OK"
|
||||||
|
|
||||||
|
# Criar diretório no servidor se não existir
|
||||||
|
info "📁 Criando diretório no servidor..."
|
||||||
|
ssh $SERVIDOR "mkdir -p ~/app_estoque"
|
||||||
|
|
||||||
|
# Enviar arquivos
|
||||||
|
info "📤 Enviando arquivos PIX..."
|
||||||
|
for arquivo in "${ARQUIVOS_ENVIAR[@]}"; do
|
||||||
|
if [ -e "$arquivo" ]; then
|
||||||
|
info "📤 Enviando: $arquivo"
|
||||||
|
rsync -avz --progress "$arquivo" $SERVIDOR:~/app_estoque/
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "✅ $arquivo enviado"
|
||||||
|
else
|
||||||
|
warn "⚠️ Erro ao enviar $arquivo"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "⚠️ Arquivo não encontrado: $arquivo"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🔧 ETAPA 3: Configurando no servidor..."
|
||||||
|
|
||||||
|
# Executar configuração no servidor
|
||||||
|
ssh $SERVIDOR << 'EOF'
|
||||||
|
cd ~/app_estoque
|
||||||
|
|
||||||
|
echo "🏦 Configurando PIX no servidor..."
|
||||||
|
|
||||||
|
# Dar permissão de execução
|
||||||
|
chmod +x configurar-pix-servidor.sh
|
||||||
|
|
||||||
|
# Instalar dependências se necessário
|
||||||
|
if [ ! -d "node_modules" ]; then
|
||||||
|
echo "📦 Instalando dependências..."
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Instalar mercadopago se não existir
|
||||||
|
if [ ! -d "node_modules/mercadopago" ]; then
|
||||||
|
echo "📦 Instalando Mercado Pago SDK..."
|
||||||
|
npm install mercadopago
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar se .env existe
|
||||||
|
if [ ! -f ".env" ]; then
|
||||||
|
echo "📝 Criando arquivo .env..."
|
||||||
|
cp .env.example .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar se credenciais PIX estão no .env
|
||||||
|
if ! grep -q "MERCADOPAGO_ACCESS_TOKEN" .env; then
|
||||||
|
echo "📝 Adicionando configurações PIX ao .env..."
|
||||||
|
cat >> .env << 'ENVEOF'
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# CONFIGURAÇÕES PIX - MERCADO PAGO
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Access Token do Mercado Pago (TEST para desenvolvimento, PROD para produção)
|
||||||
|
MERCADOPAGO_ACCESS_TOKEN=TEST-sua_access_token_aqui
|
||||||
|
|
||||||
|
# Public Key do Mercado Pago
|
||||||
|
MERCADOPAGO_PUBLIC_KEY=pk_test_sua_public_key_aqui
|
||||||
|
|
||||||
|
# URL base para webhooks (importante para produção)
|
||||||
|
BASE_URL=http://localhost:5000
|
||||||
|
ENVEOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Configuração no servidor concluída!"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "✅ Configuração no servidor concluída"
|
||||||
|
else
|
||||||
|
error "❌ Erro na configuração do servidor"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🎉 DEPLOY PIX CONCLUÍDO COM SUCESSO!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "✅ Arquivos enviados para: $SERVIDOR:~/app_estoque/"
|
||||||
|
log "✅ Dependências instaladas"
|
||||||
|
log "✅ Configurações preparadas"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
warn "📝 PRÓXIMOS PASSOS NO SERVIDOR:"
|
||||||
|
echo ""
|
||||||
|
echo "1. 🏦 CONFIGURAR CREDENCIAIS MERCADO PAGO:"
|
||||||
|
echo " ssh $SERVIDOR"
|
||||||
|
echo " cd ~/app_estoque"
|
||||||
|
echo " nano .env"
|
||||||
|
echo " # Configure MERCADOPAGO_ACCESS_TOKEN e MERCADOPAGO_PUBLIC_KEY"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "2. 🗄️ APLICAR SQL NO SUPABASE:"
|
||||||
|
echo " - Acesse: https://supabase.com/dashboard"
|
||||||
|
echo " - SQL Editor → Execute: aplicar-pix-supabase.sql"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "3. 🔄 REINICIAR SERVIDOR:"
|
||||||
|
echo " ssh $SERVIDOR"
|
||||||
|
echo " cd ~/app_estoque"
|
||||||
|
echo " pm2 restart liberi-kids"
|
||||||
|
echo " # ou: pm2 start server-supabase.js --name liberi-kids"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "4. 🧪 TESTAR PIX:"
|
||||||
|
echo " - Acesse o sistema no servidor"
|
||||||
|
echo " - Vá em Vendas → Clique no botão PIX (💳)"
|
||||||
|
echo " - Verifique se QR Code é gerado"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "💡 CREDENCIAIS MERCADO PAGO:"
|
||||||
|
echo " - Acesse: https://www.mercadopago.com.br/developers"
|
||||||
|
echo " - Crie aplicação de 'Pagamentos online'"
|
||||||
|
echo " - Copie Access Token e Public Key"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "🎉 SEU SISTEMA PIX ESTÁ PRONTO PARA CONFIGURAÇÃO FINAL!"
|
||||||
|
echo ""
|
||||||
|
warn "Execute os próximos passos no servidor para ativar o PIX!"
|
||||||
66
deploy-pix-simples.sh
Executable file
66
deploy-pix-simples.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 Deploy PIX Simples - Com Senha Interativa
|
||||||
|
# Execute: ./deploy-pix-simples.sh
|
||||||
|
|
||||||
|
echo "🚀 Deploy PIX Simples - Liberi Kids"
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# Verificar se build existe
|
||||||
|
if [ ! -d "client/build" ]; then
|
||||||
|
echo "🔨 Fazendo build do frontend..."
|
||||||
|
cd client && npm run build && cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📤 ENVIANDO ARQUIVOS PARA O SERVIDOR..."
|
||||||
|
echo "Digite a senha quando solicitado:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Enviar arquivos um por vez (mais confiável)
|
||||||
|
echo "📁 Enviando config/..."
|
||||||
|
scp -r config/ tiago@192.168.195.145:~/app_estoque/
|
||||||
|
|
||||||
|
echo "📁 Enviando client/build/..."
|
||||||
|
scp -r client/build/ tiago@192.168.195.145:~/app_estoque/client/
|
||||||
|
|
||||||
|
echo "📄 Enviando server-supabase.js..."
|
||||||
|
scp server-supabase.js tiago@192.168.195.145:~/app_estoque/
|
||||||
|
|
||||||
|
echo "📄 Enviando package.json..."
|
||||||
|
scp package.json tiago@192.168.195.145:~/app_estoque/
|
||||||
|
|
||||||
|
echo "📄 Enviando arquivos PIX..."
|
||||||
|
scp aplicar-pix-supabase.sql tiago@192.168.195.145:~/app_estoque/
|
||||||
|
scp teste-rapido-pix.sh tiago@192.168.195.145:~/app_estoque/
|
||||||
|
scp .env.example tiago@192.168.195.145:~/app_estoque/
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ ARQUIVOS ENVIADOS COM SUCESSO!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 PRÓXIMOS PASSOS NO SERVIDOR:"
|
||||||
|
echo ""
|
||||||
|
echo "1. Conectar no servidor:"
|
||||||
|
echo " ssh tiago@192.168.195.145"
|
||||||
|
echo ""
|
||||||
|
echo "2. Navegar para o diretório:"
|
||||||
|
echo " cd ~/app_estoque"
|
||||||
|
echo ""
|
||||||
|
echo "3. Instalar dependências PIX:"
|
||||||
|
echo " npm install mercadopago"
|
||||||
|
echo ""
|
||||||
|
echo "4. Configurar .env:"
|
||||||
|
echo " cp .env.example .env"
|
||||||
|
echo " nano .env"
|
||||||
|
echo " # Configure MERCADOPAGO_ACCESS_TOKEN e MERCADOPAGO_PUBLIC_KEY"
|
||||||
|
echo ""
|
||||||
|
echo "5. Aplicar SQL no Supabase:"
|
||||||
|
echo " # Execute aplicar-pix-supabase.sql no painel do Supabase"
|
||||||
|
echo ""
|
||||||
|
echo "6. Reiniciar servidor:"
|
||||||
|
echo " pm2 restart liberi-kids"
|
||||||
|
echo ""
|
||||||
|
echo "7. Testar PIX:"
|
||||||
|
echo " ./teste-rapido-pix.sh"
|
||||||
|
echo ""
|
||||||
|
echo "🎉 SEU PIX ESTARÁ FUNCIONANDO!"
|
||||||
154
deploy-to-server.sh
Executable file
154
deploy-to-server.sh
Executable file
@@ -0,0 +1,154 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 Script para Deploy Automático no Servidor via SSH
|
||||||
|
# Execute: ./deploy-to-server.sh usuario@servidor
|
||||||
|
|
||||||
|
echo "🚀 Deploy Automático - Liberi Kids"
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# Verificar se foi fornecido o servidor
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "❌ Uso: ./deploy-to-server.sh usuario@servidor"
|
||||||
|
echo " Exemplo: ./deploy-to-server.sh root@192.168.1.100"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVER=$1
|
||||||
|
PROJECT_DIR="app_estoque"
|
||||||
|
|
||||||
|
echo "🔗 Conectando ao servidor: $SERVER"
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Função para executar comando no servidor
|
||||||
|
run_remote() {
|
||||||
|
ssh $SERVER "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Função para copiar arquivos
|
||||||
|
copy_files() {
|
||||||
|
log "📁 Copiando arquivos para o servidor..."
|
||||||
|
rsync -avz --progress --exclude 'node_modules' --exclude '.git' --exclude 'client/node_modules' ./ $SERVER:~/$PROJECT_DIR/
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "✅ Arquivos copiados com sucesso!"
|
||||||
|
else
|
||||||
|
error "❌ Erro ao copiar arquivos"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Função para configurar ambiente no servidor
|
||||||
|
setup_environment() {
|
||||||
|
log "🔧 Configurando ambiente no servidor..."
|
||||||
|
|
||||||
|
run_remote "
|
||||||
|
cd $PROJECT_DIR
|
||||||
|
|
||||||
|
# Verificar se Node.js está instalado
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
echo '📦 Instalando Node.js...'
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||||
|
sudo apt-get install -y nodejs
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar versão do Node.js
|
||||||
|
NODE_VERSION=\$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
|
||||||
|
if [ \"\$NODE_VERSION\" -lt 18 ]; then
|
||||||
|
echo '❌ Node.js versão 18+ é necessária'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo '✅ Node.js \$(node -v) encontrado'
|
||||||
|
|
||||||
|
# Criar arquivo .env se não existir
|
||||||
|
if [ ! -f '.env' ]; then
|
||||||
|
echo '📝 Criando arquivo .env...'
|
||||||
|
cp .env.example .env
|
||||||
|
echo '⚠️ IMPORTANTE: Configure o arquivo .env com suas credenciais!'
|
||||||
|
echo ' Edite: nano .env'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tornar script executável
|
||||||
|
chmod +x scripts/deploy-local.sh
|
||||||
|
|
||||||
|
echo '✅ Ambiente configurado!'
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Função para fazer deploy
|
||||||
|
deploy_application() {
|
||||||
|
log "🚀 Fazendo deploy da aplicação..."
|
||||||
|
|
||||||
|
run_remote "
|
||||||
|
cd $PROJECT_DIR
|
||||||
|
|
||||||
|
# Executar script de deploy
|
||||||
|
./scripts/deploy-local.sh
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Função para verificar status
|
||||||
|
check_status() {
|
||||||
|
log "📊 Verificando status da aplicação..."
|
||||||
|
|
||||||
|
run_remote "
|
||||||
|
pm2 status
|
||||||
|
echo ''
|
||||||
|
echo '🌐 Aplicação disponível em:'
|
||||||
|
echo ' Local: http://localhost:5000'
|
||||||
|
echo ' Rede: http://\$(hostname -I | awk '{print \$1}'):5000'
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Executar deploy completo
|
||||||
|
main() {
|
||||||
|
echo ""
|
||||||
|
log "Iniciando deploy completo..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Passo 1: Copiar arquivos
|
||||||
|
copy_files
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Passo 2: Configurar ambiente
|
||||||
|
setup_environment
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Passo 3: Fazer deploy
|
||||||
|
deploy_application
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Passo 4: Verificar status
|
||||||
|
check_status
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "🎉 Deploy concluído com sucesso!"
|
||||||
|
echo ""
|
||||||
|
warn "📝 PRÓXIMOS PASSOS:"
|
||||||
|
echo "1. Configure o arquivo .env no servidor:"
|
||||||
|
echo " ssh $SERVER 'cd $PROJECT_DIR && nano .env'"
|
||||||
|
echo ""
|
||||||
|
echo "2. Reinicie a aplicação após configurar:"
|
||||||
|
echo " ssh $SERVER 'pm2 restart liberi-kids'"
|
||||||
|
echo ""
|
||||||
|
echo "3. Acesse a aplicação no navegador"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Executar função principal
|
||||||
|
main
|
||||||
56
docker-compose.yml
Normal file
56
docker-compose.yml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# 🐳 Docker Compose - Liberi Kids Sistema de Estoque
|
||||||
|
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Aplicação principal
|
||||||
|
liberi-kids:
|
||||||
|
build: .
|
||||||
|
container_name: liberi-kids-app
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=5000
|
||||||
|
- SUPABASE_URL=${SUPABASE_URL}
|
||||||
|
- SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# Volume para uploads (se necessário)
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
networks:
|
||||||
|
- liberi-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Nginx como proxy reverso (opcional)
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: liberi-kids-nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./ssl:/etc/nginx/ssl:ro
|
||||||
|
depends_on:
|
||||||
|
- liberi-kids
|
||||||
|
networks:
|
||||||
|
- liberi-network
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles:
|
||||||
|
- with-nginx
|
||||||
|
|
||||||
|
networks:
|
||||||
|
liberi-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
uploads:
|
||||||
|
driver: local
|
||||||
144
finalizar-pix.sh
Executable file
144
finalizar-pix.sh
Executable file
@@ -0,0 +1,144 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🎉 Script Final - Integração PIX Completa
|
||||||
|
# Execute: ./finalizar-pix.sh
|
||||||
|
|
||||||
|
echo "🎉 Finalizando Integração PIX - Liberi Kids"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🏦 INTEGRAÇÃO PIX IMPLEMENTADA COM SUCESSO!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "✅ Arquivos Criados:"
|
||||||
|
echo " - config/mercadopago.js (Serviço PIX)"
|
||||||
|
echo " - client/src/styles/pix-integration.css (Estilos)"
|
||||||
|
echo " - sql/add-pix-fields.sql (Migração banco)"
|
||||||
|
echo " - scripts/migrate-pix-fields.js (Script migração)"
|
||||||
|
echo " - INTEGRACAO-PIX-GUIDE.md (Documentação)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "✅ Backend Implementado:"
|
||||||
|
echo " - POST /api/pix/gerar (Gerar PIX)"
|
||||||
|
echo " - GET /api/pix/status/:id (Consultar status)"
|
||||||
|
echo " - POST /api/pix/webhook (Webhook Mercado Pago)"
|
||||||
|
echo " - GET /api/pix/pendentes (Listar pendentes)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "✅ Frontend Implementado:"
|
||||||
|
echo " - Botão PIX na lista de vendas"
|
||||||
|
echo " - Modal PIX com QR Code"
|
||||||
|
echo " - Função copiar código PIX"
|
||||||
|
echo " - Consulta automática de status"
|
||||||
|
echo " - Timer de expiração"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "✅ Build do Frontend:"
|
||||||
|
echo " - React build atualizado com PIX"
|
||||||
|
echo " - Estilos CSS incluídos"
|
||||||
|
echo " - Componentes funcionais"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
warn "📝 PRÓXIMOS PASSOS OBRIGATÓRIOS:"
|
||||||
|
echo ""
|
||||||
|
echo "1. 🏦 CRIAR CONTA MERCADO PAGO:"
|
||||||
|
echo " - Acesse: https://www.mercadopago.com.br/developers"
|
||||||
|
echo " - Crie conta de desenvolvedor"
|
||||||
|
echo " - Vá em 'Suas integrações' > 'Criar aplicação'"
|
||||||
|
echo " - Escolha 'Pagamentos online'"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "2. 📝 CONFIGURAR CREDENCIAIS:"
|
||||||
|
echo " - Copie Access Token e Public Key"
|
||||||
|
echo " - Adicione no arquivo .env do servidor:"
|
||||||
|
echo " MERCADOPAGO_ACCESS_TOKEN=TEST-sua_access_token"
|
||||||
|
echo " MERCADOPAGO_PUBLIC_KEY=pk_test_sua_public_key"
|
||||||
|
echo " BASE_URL=http://seu-servidor:5000"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "3. 🗄️ EXECUTAR MIGRAÇÃO DO BANCO:"
|
||||||
|
echo " - No servidor, execute:"
|
||||||
|
echo " node scripts/migrate-pix-fields.js"
|
||||||
|
echo " - Ou execute o SQL manualmente no Supabase"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "4. 🔄 REINICIAR SERVIDOR:"
|
||||||
|
echo " - pm2 restart liberi-kids"
|
||||||
|
echo " - Ou reinicie o servidor Node.js"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "5. 🧪 TESTAR INTEGRAÇÃO:"
|
||||||
|
echo " - Acesse uma venda no sistema"
|
||||||
|
echo " - Clique no botão PIX (ícone cartão)"
|
||||||
|
echo " - Verifique se QR Code é gerado"
|
||||||
|
echo " - Teste pagamento no ambiente de teste"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "🎯 FUNCIONALIDADES PIX DISPONÍVEIS:"
|
||||||
|
echo ""
|
||||||
|
echo " 🏦 Geração de PIX com QR Code automático"
|
||||||
|
echo " 📱 Código PIX para copiar e colar"
|
||||||
|
echo " ⏰ Expiração automática em 30 minutos"
|
||||||
|
echo " 🔔 Confirmação automática via webhook"
|
||||||
|
echo " 📊 Status em tempo real (pendente/pago/cancelado)"
|
||||||
|
echo " 📋 Lista de PIX pendentes"
|
||||||
|
echo " 💰 Integração com sistema de vendas"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "💰 CUSTOS MERCADO PAGO:"
|
||||||
|
echo " - PIX: 0,99% por transação"
|
||||||
|
echo " - Sem mensalidade"
|
||||||
|
echo " - Recebimento em 1 dia útil"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
warn "🔒 SEGURANÇA:"
|
||||||
|
echo " - Credenciais protegidas no .env"
|
||||||
|
echo " - Webhook validado"
|
||||||
|
echo " - HTTPS obrigatório em produção"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "📖 DOCUMENTAÇÃO COMPLETA:"
|
||||||
|
echo " - Consulte: INTEGRACAO-PIX-GUIDE.md"
|
||||||
|
echo " - Guia passo a passo detalhado"
|
||||||
|
echo " - Exemplos de código"
|
||||||
|
echo " - Troubleshooting"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "🎉 INTEGRAÇÃO PIX 100% IMPLEMENTADA!"
|
||||||
|
echo ""
|
||||||
|
warn "Configure as credenciais do Mercado Pago para começar a usar!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verificar se .env existe
|
||||||
|
if [ ! -f ".env" ]; then
|
||||||
|
warn "⚠️ Arquivo .env não encontrado!"
|
||||||
|
echo "Execute: cp .env.example .env"
|
||||||
|
echo "E configure suas credenciais"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🚀 Seu sistema agora aceita pagamentos PIX com QR Code automático!"
|
||||||
337
implementar-pix.sh
Executable file
337
implementar-pix.sh
Executable file
@@ -0,0 +1,337 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🏦 Script para Implementar PIX no Liberi Kids
|
||||||
|
# Execute: ./implementar-pix.sh
|
||||||
|
|
||||||
|
echo "🏦 Implementando PIX no Liberi Kids"
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo -e "${BLUE}[PIX]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Instalar dependência do Mercado Pago
|
||||||
|
log "Instalando SDK do Mercado Pago..."
|
||||||
|
npm install mercadopago
|
||||||
|
|
||||||
|
# Criar arquivo de configuração PIX
|
||||||
|
log "Criando configuração PIX..."
|
||||||
|
cat > config/mercadopago.js << 'EOF'
|
||||||
|
const mercadopago = require('mercadopago');
|
||||||
|
|
||||||
|
class MercadoPagoService {
|
||||||
|
constructor() {
|
||||||
|
if (process.env.MERCADOPAGO_ACCESS_TOKEN) {
|
||||||
|
mercadopago.configurations.setAccessToken(process.env.MERCADOPAGO_ACCESS_TOKEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async gerarPix(dados) {
|
||||||
|
try {
|
||||||
|
const payment_data = {
|
||||||
|
transaction_amount: parseFloat(dados.valor),
|
||||||
|
description: dados.descricao || `Venda #${dados.venda_id} - Liberi Kids`,
|
||||||
|
payment_method_id: 'pix',
|
||||||
|
payer: {
|
||||||
|
email: dados.cliente_email || 'cliente@liberikids.com',
|
||||||
|
first_name: dados.cliente_nome || 'Cliente',
|
||||||
|
identification: {
|
||||||
|
type: 'CPF',
|
||||||
|
number: dados.cliente_cpf || '00000000000'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
external_reference: dados.venda_id.toString(),
|
||||||
|
notification_url: `${process.env.BASE_URL || 'http://localhost:5000'}/api/pix/webhook`,
|
||||||
|
date_of_expiration: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30 minutos
|
||||||
|
};
|
||||||
|
|
||||||
|
const payment = await mercadopago.payment.create(payment_data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payment_id: payment.body.id,
|
||||||
|
qr_code: payment.body.point_of_interaction.transaction_data.qr_code,
|
||||||
|
qr_code_base64: payment.body.point_of_interaction.transaction_data.qr_code_base64,
|
||||||
|
pix_copy_paste: payment.body.point_of_interaction.transaction_data.qr_code,
|
||||||
|
expiration_date: payment.body.date_of_expiration,
|
||||||
|
transaction_amount: payment.body.transaction_amount,
|
||||||
|
status: payment.body.status
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao gerar PIX:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async consultarPagamento(payment_id) {
|
||||||
|
try {
|
||||||
|
const payment = await mercadopago.payment.get(payment_id);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
status: payment.body.status,
|
||||||
|
status_detail: payment.body.status_detail,
|
||||||
|
transaction_amount: payment.body.transaction_amount,
|
||||||
|
date_approved: payment.body.date_approved,
|
||||||
|
external_reference: payment.body.external_reference
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao consultar pagamento:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new MercadoPagoService();
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Criar CSS para PIX
|
||||||
|
log "Criando estilos PIX..."
|
||||||
|
cat > client/src/styles/pix-integration.css << 'EOF'
|
||||||
|
/* PIX Integration Styles */
|
||||||
|
|
||||||
|
.pix-modal {
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-image {
|
||||||
|
max-width: 200px;
|
||||||
|
height: auto;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-copy-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-code-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-code-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy-pix {
|
||||||
|
background: #00d4aa;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy-pix:hover {
|
||||||
|
background: #00b894;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-info {
|
||||||
|
background: #e8f5e8;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: left;
|
||||||
|
border-left: 4px solid #00d4aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-instructions {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pix {
|
||||||
|
background: #00d4aa;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pix:hover {
|
||||||
|
background: #00b894;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pix:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-status {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-status.pending {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-status.approved {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-status.rejected {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-timer {
|
||||||
|
background: #fff3cd;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 10px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pix-modal {
|
||||||
|
width: 95vw;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-image {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pix-code-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy-pix {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Atualizar .env.example
|
||||||
|
log "Atualizando .env.example com configurações PIX..."
|
||||||
|
cat >> .env.example << 'EOF'
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# CONFIGURAÇÕES PIX - MERCADO PAGO
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Access Token do Mercado Pago (TEST para desenvolvimento, PROD para produção)
|
||||||
|
MERCADOPAGO_ACCESS_TOKEN=TEST-sua_access_token_aqui
|
||||||
|
|
||||||
|
# Public Key do Mercado Pago
|
||||||
|
MERCADOPAGO_PUBLIC_KEY=pk_test_sua_public_key_aqui
|
||||||
|
|
||||||
|
# URL base para webhooks (importante para produção)
|
||||||
|
BASE_URL=http://localhost:5000
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# INSTRUÇÕES PIX
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# 1. Acesse: https://www.mercadopago.com.br/developers
|
||||||
|
# 2. Crie uma conta de desenvolvedor
|
||||||
|
# 3. Vá em "Suas integrações" > "Criar aplicação"
|
||||||
|
# 4. Escolha "Pagamentos online" e "Checkout Pro"
|
||||||
|
# 5. Copie as credenciais de TEST para desenvolvimento
|
||||||
|
# 6. Para produção, use as credenciais PROD
|
||||||
|
# 7. Configure o webhook URL no painel do Mercado Pago
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Criar script SQL para PIX
|
||||||
|
log "Criando migração do banco para PIX..."
|
||||||
|
cat > sql/add-pix-fields.sql << 'EOF'
|
||||||
|
-- Adicionar campos PIX na tabela vendas
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS status_pagamento VARCHAR(20) DEFAULT 'pendente';
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS data_pagamento TIMESTAMP;
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS pix_payment_id VARCHAR(100);
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS pix_qr_code TEXT;
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS metodo_pagamento VARCHAR(20) DEFAULT 'dinheiro';
|
||||||
|
|
||||||
|
-- Criar índices para performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vendas_status_pagamento ON vendas(status_pagamento);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vendas_pix_payment_id ON vendas(pix_payment_id);
|
||||||
|
|
||||||
|
-- Comentários para documentação
|
||||||
|
COMMENT ON COLUMN vendas.status_pagamento IS 'Status do pagamento: pendente, pago, cancelado, expirado';
|
||||||
|
COMMENT ON COLUMN vendas.data_pagamento IS 'Data e hora da confirmação do pagamento';
|
||||||
|
COMMENT ON COLUMN vendas.pix_payment_id IS 'ID do pagamento no Mercado Pago';
|
||||||
|
COMMENT ON COLUMN vendas.pix_qr_code IS 'Código PIX para copiar e colar';
|
||||||
|
COMMENT ON COLUMN vendas.metodo_pagamento IS 'Método: dinheiro, cartao, pix, transferencia';
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🎉 Implementação PIX configurada com sucesso!"
|
||||||
|
echo ""
|
||||||
|
warn "📝 PRÓXIMOS PASSOS:"
|
||||||
|
echo "1. Crie conta no Mercado Pago: https://www.mercadopago.com.br/developers"
|
||||||
|
echo "2. Configure as credenciais no arquivo .env"
|
||||||
|
echo "3. Execute a migração do banco: psql -f sql/add-pix-fields.sql"
|
||||||
|
echo "4. Adicione as rotas PIX no server-supabase.js"
|
||||||
|
echo "5. Implemente o modal PIX no frontend"
|
||||||
|
echo ""
|
||||||
|
info "📖 Consulte o arquivo INTEGRACAO-PIX-GUIDE.md para instruções completas"
|
||||||
|
echo ""
|
||||||
|
log "Arquivos criados:"
|
||||||
|
echo " - config/mercadopago.js (Serviço PIX)"
|
||||||
|
echo " - client/src/styles/pix-integration.css (Estilos)"
|
||||||
|
echo " - sql/add-pix-fields.sql (Migração banco)"
|
||||||
|
echo " - .env.example (Configurações atualizadas)"
|
||||||
46
install-nodejs-server.sh
Executable file
46
install-nodejs-server.sh
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 Script para Instalar Node.js no Servidor Ubuntu
|
||||||
|
# Execute: ./install-nodejs-server.sh usuario@servidor
|
||||||
|
|
||||||
|
echo "📦 Instalando Node.js no Servidor Ubuntu"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "❌ Uso: ./install-nodejs-server.sh usuario@servidor"
|
||||||
|
echo " Exemplo: ./install-nodejs-server.sh tiago@192.168.195.145"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVER=$1
|
||||||
|
|
||||||
|
echo "🔗 Conectando ao servidor: $SERVER"
|
||||||
|
|
||||||
|
# Instalar Node.js no servidor
|
||||||
|
ssh -t $SERVER "
|
||||||
|
echo '📦 Atualizando sistema...'
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
|
echo '📦 Instalando curl...'
|
||||||
|
sudo apt install -y curl
|
||||||
|
|
||||||
|
echo '📦 Baixando Node.js 18...'
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||||
|
|
||||||
|
echo '📦 Instalando Node.js...'
|
||||||
|
sudo apt-get install -y nodejs
|
||||||
|
|
||||||
|
echo '📦 Instalando PM2 globalmente...'
|
||||||
|
sudo npm install -g pm2
|
||||||
|
|
||||||
|
echo '✅ Verificando instalação:'
|
||||||
|
echo 'Node.js:' \$(node -v)
|
||||||
|
echo 'NPM:' \$(npm -v)
|
||||||
|
echo 'PM2:' \$(pm2 -v)
|
||||||
|
|
||||||
|
echo '🎉 Node.js instalado com sucesso!'
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Node.js instalado! Agora execute o deploy novamente:"
|
||||||
|
echo "./deploy-to-server.sh $SERVER"
|
||||||
119
limpar-servidor-completo.sh
Executable file
119
limpar-servidor-completo.sh
Executable file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🧹 Script para Limpeza COMPLETA do Servidor
|
||||||
|
# ⚠️ CUIDADO: Este script APAGA TUDO do servidor!
|
||||||
|
# Execute: ./limpar-servidor-completo.sh
|
||||||
|
|
||||||
|
echo "🧹 LIMPEZA COMPLETA DO SERVIDOR - Liberi Kids"
|
||||||
|
echo "============================================="
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[✅ OK]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[⚠️ WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo -e "${BLUE}[ℹ️ INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[❌ ERRO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configurações do servidor
|
||||||
|
SERVER_IP="192.168.195.145"
|
||||||
|
SERVER_USER="tiago"
|
||||||
|
SERVER_PATH="/home/tiago/app_estoque"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
warn "⚠️ ATENÇÃO: Este script vai APAGAR COMPLETAMENTE:"
|
||||||
|
echo " 🗂️ Todos os arquivos do projeto no servidor"
|
||||||
|
echo " 🔄 Processos PM2 do Liberi Kids"
|
||||||
|
echo " 📦 Dependências Node.js"
|
||||||
|
echo " 🔧 Configurações locais"
|
||||||
|
echo ""
|
||||||
|
warn "⚠️ O BANCO DE DADOS SUPABASE NÃO SERÁ AFETADO"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "Tem CERTEZA que quer limpar TUDO do servidor? (digite 'CONFIRMO'): " confirma
|
||||||
|
if [[ $confirma != "CONFIRMO" ]]; then
|
||||||
|
error "❌ Operação cancelada!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🧹 INICIANDO LIMPEZA COMPLETA..."
|
||||||
|
|
||||||
|
# Função para executar comandos no servidor
|
||||||
|
run_remote() {
|
||||||
|
ssh $SERVER_USER@$SERVER_IP "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "1️⃣ PARANDO PROCESSOS PM2..."
|
||||||
|
|
||||||
|
run_remote "pm2 stop all 2>/dev/null || true"
|
||||||
|
run_remote "pm2 delete all 2>/dev/null || true"
|
||||||
|
run_remote "pm2 kill 2>/dev/null || true"
|
||||||
|
|
||||||
|
log "✅ Processos PM2 parados"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "2️⃣ REMOVENDO DIRETÓRIO DO PROJETO..."
|
||||||
|
|
||||||
|
run_remote "rm -rf $SERVER_PATH"
|
||||||
|
run_remote "mkdir -p $SERVER_PATH"
|
||||||
|
|
||||||
|
log "✅ Diretório limpo: $SERVER_PATH"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "3️⃣ LIMPANDO CACHE NODE.JS..."
|
||||||
|
|
||||||
|
run_remote "npm cache clean --force 2>/dev/null || true"
|
||||||
|
run_remote "rm -rf ~/.npm 2>/dev/null || true"
|
||||||
|
|
||||||
|
log "✅ Cache Node.js limpo"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "4️⃣ REMOVENDO LOGS ANTIGOS..."
|
||||||
|
|
||||||
|
run_remote "rm -rf ~/.pm2/logs/* 2>/dev/null || true"
|
||||||
|
run_remote "rm -rf /tmp/liberi-* 2>/dev/null || true"
|
||||||
|
|
||||||
|
log "✅ Logs removidos"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "5️⃣ VERIFICANDO PORTAS..."
|
||||||
|
|
||||||
|
run_remote "pkill -f 'node.*server' 2>/dev/null || true"
|
||||||
|
run_remote "fuser -k 5000/tcp 2>/dev/null || true"
|
||||||
|
|
||||||
|
log "✅ Portas liberadas"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "🎉 LIMPEZA COMPLETA FINALIZADA!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "📋 SERVIDOR LIMPO:"
|
||||||
|
echo " ✅ Todos os arquivos removidos"
|
||||||
|
echo " ✅ Processos parados"
|
||||||
|
echo " ✅ Cache limpo"
|
||||||
|
echo " ✅ Portas liberadas"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "🚀 PRÓXIMO PASSO:"
|
||||||
|
echo " Execute: ./deploy-completo-servidor.sh"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
warn "💡 DICA: O banco Supabase não foi afetado"
|
||||||
|
warn "💡 Suas vendas e dados estão seguros na nuvem"
|
||||||
3645
package-lock.json
generated
Normal file
3645
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
package.json
Normal file
47
package.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "liberi-kids-estoque",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Sistema de controle de estoque para Liberi Kids - Moda Infantil",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server-supabase.js",
|
||||||
|
"start-sqlite": "node server.js",
|
||||||
|
"dev": "nodemon server-supabase.js",
|
||||||
|
"dev-sqlite": "nodemon server.js",
|
||||||
|
"client": "cd client && npm start",
|
||||||
|
"server": "nodemon server-supabase.js",
|
||||||
|
"build": "cd client && npm run build",
|
||||||
|
"install-client": "cd client && npm install",
|
||||||
|
"init-supabase": "node scripts/init-supabase.js",
|
||||||
|
"deploy:local": "chmod +x scripts/deploy-local.sh && ./scripts/deploy-local.sh",
|
||||||
|
"deploy:vercel": "chmod +x scripts/deploy-vercel.sh && ./scripts/deploy-vercel.sh",
|
||||||
|
"deploy:build": "npm run install-client && npm run build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/supabase-js": "^2.58.0",
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"google-auth-library": "^10.4.0",
|
||||||
|
"googleapis": "^161.0.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mercadopago": "^2.9.0",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"react-hot-toast": "^2.6.0",
|
||||||
|
"sqlite3": "^5.1.6",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.1"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"estoque",
|
||||||
|
"moda infantil",
|
||||||
|
"controle",
|
||||||
|
"vendas"
|
||||||
|
],
|
||||||
|
"author": "Liberi Kids",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
135
redeploy-completo.sh
Executable file
135
redeploy-completo.sh
Executable file
@@ -0,0 +1,135 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🔄 REDEPLOY COMPLETO - Workflow Automatizado
|
||||||
|
# Backup → Limpeza → Deploy → Teste
|
||||||
|
# Execute: ./redeploy-completo.sh
|
||||||
|
|
||||||
|
echo "🔄 REDEPLOY COMPLETO - Liberi Kids"
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[✅ OK]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[⚠️ WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo -e "${BLUE}[ℹ️ INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[❌ ERRO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "🔄 WORKFLOW COMPLETO:"
|
||||||
|
echo " 1️⃣ Backup do projeto atual"
|
||||||
|
echo " 2️⃣ Limpeza total do servidor"
|
||||||
|
echo " 3️⃣ Deploy completo"
|
||||||
|
echo " 4️⃣ Teste de funcionamento"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
warn "⚠️ ATENÇÃO: Este processo vai:"
|
||||||
|
echo " 🧹 Limpar TUDO do servidor"
|
||||||
|
echo " 🚀 Reenviar TUDO do seu PC"
|
||||||
|
echo " ⏱️ Levar alguns minutos"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "Continuar com redeploy completo? (s/N): " confirma
|
||||||
|
if [[ $confirma != "s" && $confirma != "S" ]]; then
|
||||||
|
error "❌ Redeploy cancelado!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "🚀 INICIANDO REDEPLOY COMPLETO..."
|
||||||
|
|
||||||
|
# Tornar scripts executáveis
|
||||||
|
chmod +x *.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "1️⃣ CRIANDO BACKUP..."
|
||||||
|
./backup-projeto-completo.sh
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error "❌ Falha no backup!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "✅ Backup criado com sucesso"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "2️⃣ LIMPANDO SERVIDOR..."
|
||||||
|
./limpar-servidor-completo.sh
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error "❌ Falha na limpeza!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "✅ Servidor limpo"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "3️⃣ FAZENDO DEPLOY COMPLETO..."
|
||||||
|
./deploy-completo-servidor.sh
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error "❌ Falha no deploy!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "✅ Deploy realizado"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "4️⃣ TESTANDO FUNCIONAMENTO..."
|
||||||
|
|
||||||
|
# Aguardar servidor inicializar
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Testar conexão
|
||||||
|
if curl -s http://192.168.195.145:5000 > /dev/null; then
|
||||||
|
log "✅ Servidor respondendo"
|
||||||
|
else
|
||||||
|
warn "⚠️ Servidor pode não estar respondendo"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Testar API
|
||||||
|
if curl -s http://192.168.195.145:5000/api/produtos > /dev/null; then
|
||||||
|
log "✅ API funcionando"
|
||||||
|
else
|
||||||
|
warn "⚠️ API pode não estar funcionando"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "🎉 REDEPLOY COMPLETO FINALIZADO!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "📋 RESUMO:"
|
||||||
|
echo " ✅ Backup criado"
|
||||||
|
echo " ✅ Servidor limpo"
|
||||||
|
echo " ✅ Deploy realizado"
|
||||||
|
echo " ✅ Testes executados"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "🌐 ACESSO:"
|
||||||
|
echo " Frontend: http://192.168.195.145:5000"
|
||||||
|
echo " SSH: ssh tiago@192.168.195.145"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
info "🔧 VERIFICAÇÕES FINAIS:"
|
||||||
|
echo " 1. Acesse o sistema no navegador"
|
||||||
|
echo " 2. Teste login e funcionalidades"
|
||||||
|
echo " 3. Verifique se PIX está funcionando"
|
||||||
|
echo " 4. Confirme se dados estão corretos"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
warn "💡 LEMBRE-SE:"
|
||||||
|
echo " - Configurar credenciais se necessário"
|
||||||
|
echo " - Aplicar SQL no Supabase se for primeira vez"
|
||||||
|
echo " - Testar todas as funcionalidades importantes"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "✨ SEU PROJETO FOI COMPLETAMENTE REDEPLOYADO!"
|
||||||
127
scripts/deploy-local.sh
Executable file
127
scripts/deploy-local.sh
Executable file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 Script de Deploy Local - Liberi Kids
|
||||||
|
# Execute: chmod +x scripts/deploy-local.sh && ./scripts/deploy-local.sh
|
||||||
|
|
||||||
|
echo "🚀 Iniciando deploy local do Liberi Kids..."
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Função para log colorido
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verificar se Node.js está instalado
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
error "Node.js não encontrado. Instale Node.js 18+ primeiro."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar versão do Node.js
|
||||||
|
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
|
||||||
|
if [ "$NODE_VERSION" -lt 18 ]; then
|
||||||
|
error "Node.js versão 18+ é necessária. Versão atual: $(node -v)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Node.js $(node -v) encontrado ✓"
|
||||||
|
|
||||||
|
# Instalar dependências do servidor
|
||||||
|
log "Instalando dependências do servidor..."
|
||||||
|
npm install
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error "Falha ao instalar dependências do servidor"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Entrar na pasta do cliente
|
||||||
|
cd client
|
||||||
|
|
||||||
|
# Instalar dependências do cliente
|
||||||
|
log "Instalando dependências do frontend..."
|
||||||
|
npm install
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error "Falha ao instalar dependências do frontend"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fazer build do frontend
|
||||||
|
log "Fazendo build do frontend..."
|
||||||
|
npm run build
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error "Falha no build do frontend"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Voltar para pasta raiz
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Verificar se arquivo .env existe
|
||||||
|
if [ ! -f ".env" ]; then
|
||||||
|
warn "Arquivo .env não encontrado. Criando exemplo..."
|
||||||
|
cat > .env << EOF
|
||||||
|
# Configurações do Supabase
|
||||||
|
SUPABASE_URL=https://seu-projeto.supabase.co
|
||||||
|
SUPABASE_ANON_KEY=sua_chave_anonima_aqui
|
||||||
|
|
||||||
|
# Configurações do servidor
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=5000
|
||||||
|
EOF
|
||||||
|
warn "Configure o arquivo .env com suas credenciais do Supabase!"
|
||||||
|
echo "Edite o arquivo .env antes de continuar."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar se PM2 está instalado
|
||||||
|
if ! command -v pm2 &> /dev/null; then
|
||||||
|
log "PM2 não encontrado. Instalando PM2..."
|
||||||
|
npm install -g pm2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parar processo anterior se existir
|
||||||
|
pm2 delete liberi-kids 2>/dev/null || true
|
||||||
|
|
||||||
|
# Iniciar aplicação com PM2
|
||||||
|
log "Iniciando aplicação com PM2..."
|
||||||
|
pm2 start server-supabase.js --name "liberi-kids" --env production
|
||||||
|
|
||||||
|
# Configurar PM2 para iniciar automaticamente
|
||||||
|
log "Configurando PM2 para inicialização automática..."
|
||||||
|
pm2 startup
|
||||||
|
pm2 save
|
||||||
|
|
||||||
|
# Status final
|
||||||
|
log "Deploy concluído com sucesso! 🎉"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}📊 Status da aplicação:${NC}"
|
||||||
|
pm2 status
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}🌐 Acesso:${NC}"
|
||||||
|
echo " Local: http://localhost:5000"
|
||||||
|
echo " Rede: http://$(hostname -I | awk '{print $1}'):5000"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}📋 Comandos úteis:${NC}"
|
||||||
|
echo " Ver logs: pm2 logs liberi-kids"
|
||||||
|
echo " Reiniciar: pm2 restart liberi-kids"
|
||||||
|
echo " Parar: pm2 stop liberi-kids"
|
||||||
|
echo " Status: pm2 status"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "Aplicação rodando em produção! ✓"
|
||||||
126
scripts/deploy-vercel.sh
Executable file
126
scripts/deploy-vercel.sh
Executable file
@@ -0,0 +1,126 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 Script de Deploy Vercel - Liberi Kids
|
||||||
|
# Execute: chmod +x scripts/deploy-vercel.sh && ./scripts/deploy-vercel.sh
|
||||||
|
|
||||||
|
echo "☁️ Iniciando deploy no Vercel..."
|
||||||
|
|
||||||
|
# Cores para output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verificar se Vercel CLI está instalado
|
||||||
|
if ! command -v vercel &> /dev/null; then
|
||||||
|
log "Instalando Vercel CLI..."
|
||||||
|
npm install -g vercel
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Criar arquivo vercel.json se não existir
|
||||||
|
if [ ! -f "vercel.json" ]; then
|
||||||
|
log "Criando configuração do Vercel..."
|
||||||
|
cat > vercel.json << 'EOF'
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"name": "liberi-kids-estoque",
|
||||||
|
"builds": [
|
||||||
|
{
|
||||||
|
"src": "server-supabase.js",
|
||||||
|
"use": "@vercel/node"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "client/package.json",
|
||||||
|
"use": "@vercel/static-build",
|
||||||
|
"config": {
|
||||||
|
"distDir": "build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"src": "/api/(.*)",
|
||||||
|
"dest": "/server-supabase.js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/(.*)",
|
||||||
|
"dest": "/client/build/$1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Criar arquivo .vercelignore se não existir
|
||||||
|
if [ ! -f ".vercelignore" ]; then
|
||||||
|
log "Criando .vercelignore..."
|
||||||
|
cat > .vercelignore << 'EOF'
|
||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development
|
||||||
|
.env.test
|
||||||
|
.env.production
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.DS_Store
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar se as dependências estão instaladas
|
||||||
|
log "Verificando dependências..."
|
||||||
|
npm install
|
||||||
|
|
||||||
|
cd client
|
||||||
|
npm install
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Fazer login no Vercel (se necessário)
|
||||||
|
log "Verificando login no Vercel..."
|
||||||
|
vercel whoami || {
|
||||||
|
log "Faça login no Vercel:"
|
||||||
|
vercel login
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
log "Fazendo deploy no Vercel..."
|
||||||
|
vercel --prod
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "Deploy concluído com sucesso! 🎉"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}📋 Próximos passos:${NC}"
|
||||||
|
echo "1. Configure as variáveis de ambiente no dashboard do Vercel:"
|
||||||
|
echo " - SUPABASE_URL"
|
||||||
|
echo " - SUPABASE_ANON_KEY"
|
||||||
|
echo ""
|
||||||
|
echo "2. Acesse: https://vercel.com/dashboard"
|
||||||
|
echo "3. Selecione seu projeto"
|
||||||
|
echo "4. Vá em Settings > Environment Variables"
|
||||||
|
echo "5. Adicione suas variáveis do Supabase"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✓ Aplicação disponível na URL fornecida pelo Vercel${NC}"
|
||||||
|
else
|
||||||
|
error "Falha no deploy. Verifique os logs acima."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
84
scripts/fix-devolucoes.js
Executable file
84
scripts/fix-devolucoes.js
Executable file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
async function limparDevolucoesDuplicadas() {
|
||||||
|
try {
|
||||||
|
console.log('🔧 Iniciando limpeza de devoluções duplicadas...');
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:5000/api/devolucoes/limpar-duplicadas', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`✅ ${result.message}`);
|
||||||
|
console.log(`📊 Total removidas: ${result.removidas}`);
|
||||||
|
} else {
|
||||||
|
console.log('❌ Erro:', result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro ao limpar devoluções:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verificarEstadoDevolucoes() {
|
||||||
|
try {
|
||||||
|
console.log('\n📋 Verificando estado atual das devoluções...');
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:5000/api/devolucoes');
|
||||||
|
const devolucoes = await response.json();
|
||||||
|
|
||||||
|
console.log(`📊 Total de devoluções: ${devolucoes.length}`);
|
||||||
|
|
||||||
|
// Agrupar por venda para verificar duplicatas
|
||||||
|
const porVenda = {};
|
||||||
|
devolucoes.forEach(dev => {
|
||||||
|
const key = dev.venda_id;
|
||||||
|
if (!porVenda[key]) {
|
||||||
|
porVenda[key] = [];
|
||||||
|
}
|
||||||
|
porVenda[key].push(dev);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📊 Vendas com devoluções: ${Object.keys(porVenda).length}`);
|
||||||
|
|
||||||
|
// Verificar vendas com múltiplas devoluções
|
||||||
|
const vendasComMultiplas = Object.entries(porVenda)
|
||||||
|
.filter(([_, devs]) => devs.length > 1);
|
||||||
|
|
||||||
|
if (vendasComMultiplas.length > 0) {
|
||||||
|
console.log(`⚠️ Vendas com múltiplas devoluções: ${vendasComMultiplas.length}`);
|
||||||
|
vendasComMultiplas.forEach(([vendaId, devs]) => {
|
||||||
|
console.log(` - Venda ${vendaId}: ${devs.length} devoluções`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('✅ Nenhuma venda com múltiplas devoluções encontrada');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro ao verificar devoluções:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('🔧 Sistema de Correção de Devoluções/Trocas - Liberi Kids');
|
||||||
|
console.log('======================================================');
|
||||||
|
|
||||||
|
await verificarEstadoDevolucoes();
|
||||||
|
await limparDevolucoesDuplicadas();
|
||||||
|
await verificarEstadoDevolucoes();
|
||||||
|
|
||||||
|
console.log('\n✅ Processo concluído!');
|
||||||
|
console.log('\n📋 Próximos passos:');
|
||||||
|
console.log('1. Teste uma devolução simples');
|
||||||
|
console.log('2. Teste uma troca de produto');
|
||||||
|
console.log('3. Verifique se o estoque está sendo atualizado corretamente');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
58
scripts/fix-google-drive.sh
Executable file
58
scripts/fix-google-drive.sh
Executable file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🔧 Script de Correção Google Drive - Liberi Kids"
|
||||||
|
echo "=============================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "📋 Checklist de Configuração:"
|
||||||
|
echo ""
|
||||||
|
echo "1. ✅ Verificar se Google Drive API está ativa"
|
||||||
|
echo " - Acesse: https://console.cloud.google.com/apis/library/drive.googleapis.com"
|
||||||
|
echo " - Clique em 'Ativar' se não estiver ativo"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "2. ✅ Configurar Tela de Consentimento OAuth"
|
||||||
|
echo " - Acesse: https://console.cloud.google.com/apis/credentials/consent"
|
||||||
|
echo " - Tipo: Externo"
|
||||||
|
echo " - Nome do app: Liberi Kids - Sistema de Estoque"
|
||||||
|
echo " - Adicionar escopos: drive.file e drive.readonly"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "3. 🎯 CRÍTICO: Adicionar Usuários de Teste"
|
||||||
|
echo " - Na tela de consentimento, seção 'Usuários de teste'"
|
||||||
|
echo " - Adicionar SEU EMAIL (o mesmo que vai usar para autorizar)"
|
||||||
|
echo " - Sem isso, você receberá erro de autorização"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "4. ✅ Verificar Credenciais OAuth"
|
||||||
|
echo " - Acesse: https://console.cloud.google.com/apis/credentials"
|
||||||
|
echo " - URIs de redirecionamento: http://localhost:5000/auth/google-drive/callback"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "5. 🔄 Testar Configuração"
|
||||||
|
echo " - Recarregar página de Configurações"
|
||||||
|
echo " - Tentar autorizar novamente"
|
||||||
|
echo " - Usar o email que foi adicionado como usuário de teste"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "📞 URLs Importantes:"
|
||||||
|
echo " - Google Cloud Console: https://console.cloud.google.com/"
|
||||||
|
echo " - APIs & Services: https://console.cloud.google.com/apis/"
|
||||||
|
echo " - OAuth Consent: https://console.cloud.google.com/apis/credentials/consent"
|
||||||
|
echo " - Credentials: https://console.cloud.google.com/apis/credentials"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "🚨 Lembre-se: Use o MESMO EMAIL em 'Usuários de teste' e na autorização!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verificar se o servidor está rodando
|
||||||
|
if curl -s http://localhost:5000/api/google-drive/status > /dev/null; then
|
||||||
|
echo "✅ Servidor está rodando"
|
||||||
|
echo "🔗 Acesse: http://localhost:3000/configuracoes"
|
||||||
|
else
|
||||||
|
echo "❌ Servidor não está rodando"
|
||||||
|
echo "Execute: npm start"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Após configurar no Google Cloud Console, recarregue a página e tente novamente!"
|
||||||
99
scripts/init-supabase.js
Normal file
99
scripts/init-supabase.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
const supabase = require('../config/supabase');
|
||||||
|
|
||||||
|
async function initializeSupabaseTables() {
|
||||||
|
console.log('🚀 Inicializando tabelas no Supabase...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Criar tabela de fornecedores
|
||||||
|
console.log('📦 Criando tabela fornecedores...');
|
||||||
|
const { error: fornecedoresError } = await supabase.rpc('create_fornecedores_table');
|
||||||
|
if (fornecedoresError && !fornecedoresError.message.includes('already exists')) {
|
||||||
|
console.error('Erro ao criar tabela fornecedores:', fornecedoresError);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Tabela fornecedores criada/verificada');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Criar tabela de produtos
|
||||||
|
console.log('📦 Criando tabela produtos...');
|
||||||
|
const { error: produtosError } = await supabase.rpc('create_produtos_table');
|
||||||
|
if (produtosError && !produtosError.message.includes('already exists')) {
|
||||||
|
console.error('Erro ao criar tabela produtos:', produtosError);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Tabela produtos criada/verificada');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Criar tabela de variações de produtos
|
||||||
|
console.log('📦 Criando tabela produto_variacoes...');
|
||||||
|
const { error: variacoesError } = await supabase.rpc('create_produto_variacoes_table');
|
||||||
|
if (variacoesError && !variacoesError.message.includes('already exists')) {
|
||||||
|
console.error('Erro ao criar tabela produto_variacoes:', variacoesError);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Tabela produto_variacoes criada/verificada');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Criar tabela de clientes
|
||||||
|
console.log('📦 Criando tabela clientes...');
|
||||||
|
const { error: clientesError } = await supabase.rpc('create_clientes_table');
|
||||||
|
if (clientesError && !clientesError.message.includes('already exists')) {
|
||||||
|
console.error('Erro ao criar tabela clientes:', clientesError);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Tabela clientes criada/verificada');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Criar tabela de tipos de despesas
|
||||||
|
console.log('📦 Criando tabela tipos_despesas...');
|
||||||
|
const { error: tiposDespesasError } = await supabase.rpc('create_tipos_despesas_table');
|
||||||
|
if (tiposDespesasError && !tiposDespesasError.message.includes('already exists')) {
|
||||||
|
console.error('Erro ao criar tabela tipos_despesas:', tiposDespesasError);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Tabela tipos_despesas criada/verificada');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Criar tabela de despesas
|
||||||
|
console.log('📦 Criando tabela despesas...');
|
||||||
|
const { error: despesasError } = await supabase.rpc('create_despesas_table');
|
||||||
|
if (despesasError && !despesasError.message.includes('already exists')) {
|
||||||
|
console.error('Erro ao criar tabela despesas:', despesasError);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Tabela despesas criada/verificada');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Criar tabela de vendas
|
||||||
|
console.log('📦 Criando tabela vendas...');
|
||||||
|
const { error: vendasError } = await supabase.rpc('create_vendas_table');
|
||||||
|
if (vendasError && !vendasError.message.includes('already exists')) {
|
||||||
|
console.error('Erro ao criar tabela vendas:', vendasError);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Tabela vendas criada/verificada');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Criar tabela de itens de venda
|
||||||
|
console.log('📦 Criando tabela venda_itens...');
|
||||||
|
const { error: vendaItensError } = await supabase.rpc('create_venda_itens_table');
|
||||||
|
if (vendaItensError && !vendaItensError.message.includes('already exists')) {
|
||||||
|
console.error('Erro ao criar tabela venda_itens:', vendaItensError);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Tabela venda_itens criada/verificada');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎉 Inicialização do Supabase concluída!');
|
||||||
|
|
||||||
|
// Testar conexão
|
||||||
|
const { data, error } = await supabase.from('fornecedores').select('count', { count: 'exact' });
|
||||||
|
if (error) {
|
||||||
|
console.error('❌ Erro ao testar conexão:', error);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Conexão com Supabase funcionando!');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro geral na inicialização:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executar se chamado diretamente
|
||||||
|
if (require.main === module) {
|
||||||
|
initializeSupabaseTables();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = initializeSupabaseTables;
|
||||||
72
scripts/migrate-despesas-text-fields.js
Normal file
72
scripts/migrate-despesas-text-fields.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
const supabase = require('../config/supabase');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
async function migrateDespesasToTextFields() {
|
||||||
|
console.log('🔄 Migrando tabela despesas para campos de texto livre...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ler o script SQL
|
||||||
|
const sqlScript = fs.readFileSync(
|
||||||
|
path.join(__dirname, '../sql/alter-despesas-text-fields.sql'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dividir o script em comandos individuais
|
||||||
|
const commands = sqlScript
|
||||||
|
.split(';')
|
||||||
|
.map(cmd => cmd.trim())
|
||||||
|
.filter(cmd => cmd.length > 0 && !cmd.startsWith('--'));
|
||||||
|
|
||||||
|
console.log(`📝 Executando ${commands.length} comandos SQL...`);
|
||||||
|
|
||||||
|
// Executar cada comando
|
||||||
|
for (let i = 0; i < commands.length; i++) {
|
||||||
|
const command = commands[i];
|
||||||
|
console.log(`⚡ Executando comando ${i + 1}/${commands.length}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { error } = await supabase.rpc('exec_sql', { sql_query: command });
|
||||||
|
if (error) {
|
||||||
|
console.log(`⚠️ Comando ${i + 1} com aviso:`, error.message);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Comando ${i + 1} executado com sucesso`);
|
||||||
|
}
|
||||||
|
} catch (cmdError) {
|
||||||
|
console.log(`⚠️ Erro no comando ${i + 1}:`, cmdError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se as colunas foram criadas
|
||||||
|
console.log('🔍 Verificando estrutura da tabela...');
|
||||||
|
const { data: columns, error: columnsError } = await supabase
|
||||||
|
.from('information_schema.columns')
|
||||||
|
.select('column_name')
|
||||||
|
.eq('table_name', 'despesas')
|
||||||
|
.in('column_name', ['tipo_nome', 'fornecedor_nome']);
|
||||||
|
|
||||||
|
if (columnsError) {
|
||||||
|
console.log('⚠️ Não foi possível verificar as colunas:', columnsError.message);
|
||||||
|
} else {
|
||||||
|
const columnNames = columns.map(col => col.column_name);
|
||||||
|
if (columnNames.includes('tipo_nome') && columnNames.includes('fornecedor_nome')) {
|
||||||
|
console.log('✅ Colunas tipo_nome e fornecedor_nome criadas com sucesso!');
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ Algumas colunas podem não ter sido criadas:', columnNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎉 Migração concluída!');
|
||||||
|
console.log('📋 Agora você pode usar campos de texto livre para Tipo de Despesa e Fornecedor');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro durante a migração:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executar se chamado diretamente
|
||||||
|
if (require.main === module) {
|
||||||
|
migrateDespesasToTextFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = migrateDespesasToTextFields;
|
||||||
60
scripts/migrate-pix-fields.js
Normal file
60
scripts/migrate-pix-fields.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const { createClient } = require('@supabase/supabase-js');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const supabase = createClient(
|
||||||
|
process.env.SUPABASE_URL,
|
||||||
|
process.env.SUPABASE_ANON_KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
async function migratePIXFields() {
|
||||||
|
console.log('🏦 Iniciando migração dos campos PIX...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Executar as alterações na tabela vendas
|
||||||
|
const queries = [
|
||||||
|
"ALTER TABLE vendas ADD COLUMN IF NOT EXISTS status_pagamento VARCHAR(20) DEFAULT 'pendente'",
|
||||||
|
"ALTER TABLE vendas ADD COLUMN IF NOT EXISTS data_pagamento TIMESTAMP",
|
||||||
|
"ALTER TABLE vendas ADD COLUMN IF NOT EXISTS pix_payment_id VARCHAR(100)",
|
||||||
|
"ALTER TABLE vendas ADD COLUMN IF NOT EXISTS pix_qr_code TEXT",
|
||||||
|
"ALTER TABLE vendas ADD COLUMN IF NOT EXISTS metodo_pagamento VARCHAR(20) DEFAULT 'dinheiro'"
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const query of queries) {
|
||||||
|
console.log(`Executando: ${query}`);
|
||||||
|
const { error } = await supabase.rpc('exec_sql', { sql: query });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error(`Erro ao executar query: ${error.message}`);
|
||||||
|
// Continuar mesmo com erro (campo pode já existir)
|
||||||
|
} else {
|
||||||
|
console.log('✅ Query executada com sucesso');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Criar índices
|
||||||
|
const indexes = [
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_vendas_status_pagamento ON vendas(status_pagamento)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_vendas_pix_payment_id ON vendas(pix_payment_id)"
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const index of indexes) {
|
||||||
|
console.log(`Criando índice: ${index}`);
|
||||||
|
const { error } = await supabase.rpc('exec_sql', { sql: index });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error(`Erro ao criar índice: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Índice criado com sucesso');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎉 Migração PIX concluída com sucesso!');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro na migração:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executar migração
|
||||||
|
migratePIXFields();
|
||||||
4030
server-supabase.js
Normal file
4030
server-supabase.js
Normal file
File diff suppressed because it is too large
Load Diff
635
server.js
Normal file
635
server.js
Normal file
@@ -0,0 +1,635 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const path = require('path');
|
||||||
|
const multer = require('multer');
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 5000;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use('/uploads', express.static('uploads'));
|
||||||
|
app.use(express.static(path.join(__dirname, 'client/build')));
|
||||||
|
|
||||||
|
// Configuração do multer para upload de imagens
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
cb(null, 'uploads/');
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const uniqueName = `${Date.now()}-${uuidv4()}${path.extname(file.originalname)}`;
|
||||||
|
cb(null, uniqueName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
if (file.mimetype.startsWith('image/')) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Apenas imagens são permitidas!'), false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limits: { fileSize: 5 * 1024 * 1024 } // 5MB
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inicializar banco de dados
|
||||||
|
const db = new sqlite3.Database('./liberi_kids.db', (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Erro ao conectar com o banco de dados:', err.message);
|
||||||
|
} else {
|
||||||
|
console.log('Conectado ao banco de dados SQLite.');
|
||||||
|
initializeDatabase();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Função para inicializar as tabelas
|
||||||
|
function initializeDatabase() {
|
||||||
|
// Tabela de produtos
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS produtos (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
id_produto TEXT,
|
||||||
|
marca TEXT NOT NULL,
|
||||||
|
nome TEXT NOT NULL,
|
||||||
|
estacao TEXT NOT NULL,
|
||||||
|
genero TEXT DEFAULT 'Unissex',
|
||||||
|
fornecedor_id TEXT,
|
||||||
|
valor_compra REAL NOT NULL,
|
||||||
|
valor_revenda REAL NOT NULL,
|
||||||
|
foto_principal_url TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (fornecedor_id) REFERENCES fornecedores (id)
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Tabela de variações de produtos (tamanho, cor, quantidade)
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS produto_variacoes (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
produto_id TEXT NOT NULL,
|
||||||
|
tamanho TEXT NOT NULL,
|
||||||
|
cor TEXT NOT NULL,
|
||||||
|
quantidade INTEGER NOT NULL DEFAULT 0,
|
||||||
|
foto_url TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (produto_id) REFERENCES produtos (id) ON DELETE CASCADE
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Tabela de clientes
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS clientes (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
nome_completo TEXT NOT NULL,
|
||||||
|
email TEXT,
|
||||||
|
telefone TEXT,
|
||||||
|
endereco TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Tabela de fornecedores
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS fornecedores (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
razao_social TEXT NOT NULL,
|
||||||
|
telefone TEXT,
|
||||||
|
whatsapp TEXT,
|
||||||
|
endereco TEXT,
|
||||||
|
email TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Tabela de tipos de despesas
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS tipos_despesas (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
nome TEXT NOT NULL UNIQUE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Tabela de despesas
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS despesas (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
tipo_despesa_id TEXT NOT NULL,
|
||||||
|
fornecedor_id TEXT,
|
||||||
|
data DATE NOT NULL,
|
||||||
|
valor REAL NOT NULL,
|
||||||
|
descricao TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (tipo_despesa_id) REFERENCES tipos_despesas (id),
|
||||||
|
FOREIGN KEY (fornecedor_id) REFERENCES fornecedores (id)
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Tabela de vendas
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS vendas (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
cliente_id TEXT,
|
||||||
|
tipo_pagamento TEXT NOT NULL, -- 'vista' ou 'parcelado'
|
||||||
|
valor_total REAL NOT NULL,
|
||||||
|
desconto REAL DEFAULT 0,
|
||||||
|
parcelas INTEGER DEFAULT 1,
|
||||||
|
valor_parcela REAL,
|
||||||
|
data_venda DATE NOT NULL,
|
||||||
|
status TEXT DEFAULT 'concluida', -- 'concluida', 'cancelada'
|
||||||
|
observacoes TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (cliente_id) REFERENCES clientes (id)
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Tabela de itens da venda
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS venda_itens (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
venda_id TEXT NOT NULL,
|
||||||
|
produto_variacao_id TEXT NOT NULL,
|
||||||
|
quantidade INTEGER NOT NULL,
|
||||||
|
valor_unitario REAL NOT NULL,
|
||||||
|
valor_total REAL NOT NULL,
|
||||||
|
FOREIGN KEY (venda_id) REFERENCES vendas (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (produto_variacao_id) REFERENCES produto_variacoes (id)
|
||||||
|
)`);
|
||||||
|
|
||||||
|
console.log('Tabelas do banco de dados inicializadas.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotas da API
|
||||||
|
|
||||||
|
// === PRODUTOS ===
|
||||||
|
app.get('/api/produtos', (req, res) => {
|
||||||
|
const query = `
|
||||||
|
SELECT p.*, f.razao_social as fornecedor_nome,
|
||||||
|
COUNT(pv.id) as total_variacoes,
|
||||||
|
SUM(pv.quantidade) as estoque_total
|
||||||
|
FROM produtos p
|
||||||
|
LEFT JOIN fornecedores f ON p.fornecedor_id = f.id
|
||||||
|
LEFT JOIN produto_variacoes pv ON p.id = pv.produto_id
|
||||||
|
GROUP BY p.id
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.all(query, [], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/produtos', upload.any(), (req, res) => {
|
||||||
|
console.log('Recebendo requisição para criar produto:', req.body);
|
||||||
|
console.log('Arquivos recebidos:', req.files ? req.files.length : 0);
|
||||||
|
|
||||||
|
const { id_produto, marca, nome, estacao, genero, fornecedor_id, valor_compra, valor_revenda, variacoes_data } = req.body;
|
||||||
|
const produtoId = uuidv4();
|
||||||
|
|
||||||
|
// Validações básicas
|
||||||
|
if (!marca || !nome || !estacao || !valor_compra || !valor_revenda) {
|
||||||
|
return res.status(400).json({ error: 'Campos obrigatórios não preenchidos' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse das variações
|
||||||
|
let variacoes = [];
|
||||||
|
try {
|
||||||
|
if (variacoes_data) {
|
||||||
|
variacoes = JSON.parse(variacoes_data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao fazer parse das variações:', error);
|
||||||
|
return res.status(400).json({ error: 'Dados de variações inválidos' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variacoes.length === 0) {
|
||||||
|
return res.status(400).json({ error: 'Pelo menos uma variação é obrigatória' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iniciar transação
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
|
// Inserir produto
|
||||||
|
const produtoQuery = `INSERT INTO produtos (id, id_produto, marca, nome, estacao, genero, fornecedor_id, valor_compra, valor_revenda)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||||
|
|
||||||
|
const fornecedorIdFinal = fornecedor_id && fornecedor_id !== '' ? fornecedor_id : null;
|
||||||
|
|
||||||
|
db.run(produtoQuery, [produtoId, id_produto, marca, nome, estacao, genero || 'Unissex', fornecedorIdFinal, valor_compra, valor_revenda], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Erro ao inserir produto:', err);
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
return res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Produto inserido com sucesso, processando variações...');
|
||||||
|
|
||||||
|
// Processar variações e fotos
|
||||||
|
let variacoesProcessadas = 0;
|
||||||
|
const totalVariacoes = variacoes.length;
|
||||||
|
|
||||||
|
if (totalVariacoes === 0) {
|
||||||
|
db.run('COMMIT');
|
||||||
|
return res.json({ id: produtoId, message: 'Produto criado com sucesso!' });
|
||||||
|
}
|
||||||
|
|
||||||
|
variacoes.forEach((variacao, varIndex) => {
|
||||||
|
const variacaoId = uuidv4();
|
||||||
|
|
||||||
|
// Inserir variação
|
||||||
|
const variacaoQuery = `INSERT INTO produto_variacoes (id, produto_id, tamanho, cor, quantidade)
|
||||||
|
VALUES (?, ?, ?, ?, ?)`;
|
||||||
|
|
||||||
|
db.run(variacaoQuery, [variacaoId, produtoId, variacao.tamanho, variacao.cor, variacao.quantidade], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Erro ao inserir variação:', err);
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
return res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processar fotos desta variação
|
||||||
|
const fotosVariacao = req.files ? req.files.filter(file =>
|
||||||
|
file.fieldname.startsWith(`variacao_${varIndex}_foto_`)
|
||||||
|
) : [];
|
||||||
|
|
||||||
|
if (fotosVariacao.length > 0) {
|
||||||
|
// Usar a primeira foto como foto_url da variação
|
||||||
|
const primeiraFoto = fotosVariacao[0];
|
||||||
|
const fotoUrl = `/uploads/${primeiraFoto.filename}`;
|
||||||
|
|
||||||
|
// Atualizar variação com a primeira foto
|
||||||
|
db.run('UPDATE produto_variacoes SET foto_url = ? WHERE id = ?', [fotoUrl, variacaoId], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Erro ao atualizar foto da variação:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
variacoesProcessadas++;
|
||||||
|
console.log(`Variação processada: ${variacoesProcessadas}/${totalVariacoes}`);
|
||||||
|
|
||||||
|
if (variacoesProcessadas === totalVariacoes) {
|
||||||
|
db.run('COMMIT');
|
||||||
|
console.log('Produto criado com sucesso!');
|
||||||
|
res.json({ id: produtoId, message: 'Produto criado com sucesso!' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/produtos/:id', upload.single('foto_principal'), (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { id_produto, marca, nome, estacao, genero, fornecedor_id, valor_compra, valor_revenda } = req.body;
|
||||||
|
|
||||||
|
let query, params;
|
||||||
|
|
||||||
|
if (req.file) {
|
||||||
|
const foto_principal_url = `/uploads/${req.file.filename}`;
|
||||||
|
query = `UPDATE produtos
|
||||||
|
SET id_produto = ?, marca = ?, nome = ?, estacao = ?, genero = ?, fornecedor_id = ?, valor_compra = ?, valor_revenda = ?, foto_principal_url = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?`;
|
||||||
|
params = [id_produto, marca, nome, estacao, genero || 'Unissex', fornecedor_id, valor_compra, valor_revenda, foto_principal_url, id];
|
||||||
|
} else {
|
||||||
|
query = `UPDATE produtos
|
||||||
|
SET id_produto = ?, marca = ?, nome = ?, estacao = ?, genero = ?, fornecedor_id = ?, valor_compra = ?, valor_revenda = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?`;
|
||||||
|
params = [id_produto, marca, nome, estacao, genero || 'Unissex', fornecedor_id, valor_compra, valor_revenda, id];
|
||||||
|
}
|
||||||
|
|
||||||
|
db.run(query, params, function(err) {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ message: 'Produto atualizado com sucesso!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === VARIAÇÕES DE PRODUTOS ===
|
||||||
|
app.get('/api/produtos/:id/variacoes', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
db.all('SELECT * FROM produto_variacoes WHERE produto_id = ? ORDER BY tamanho, cor', [id], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/produtos/:id/variacoes', upload.single('foto'), (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { tamanho, cor, quantidade } = req.body;
|
||||||
|
const foto_url = req.file ? `/uploads/${req.file.filename}` : null;
|
||||||
|
const variacao_id = uuidv4();
|
||||||
|
|
||||||
|
const query = `INSERT INTO produto_variacoes (id, produto_id, tamanho, cor, quantidade, foto_url)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||||
|
|
||||||
|
db.run(query, [variacao_id, id, tamanho, cor, quantidade, foto_url], function(err) {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ id: variacao_id, message: 'Variação adicionada com sucesso!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === CLIENTES ===
|
||||||
|
app.get('/api/clientes', (req, res) => {
|
||||||
|
db.all('SELECT * FROM clientes ORDER BY nome_completo', [], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/clientes', (req, res) => {
|
||||||
|
const { nome_completo, email, telefone, endereco } = req.body;
|
||||||
|
const id = uuidv4();
|
||||||
|
|
||||||
|
const query = `INSERT INTO clientes (id, nome_completo, email, telefone, endereco)
|
||||||
|
VALUES (?, ?, ?, ?, ?)`;
|
||||||
|
|
||||||
|
db.run(query, [id, nome_completo, email, telefone, endereco], function(err) {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ id, message: 'Cliente cadastrado com sucesso!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === FORNECEDORES ===
|
||||||
|
app.get('/api/fornecedores', (req, res) => {
|
||||||
|
db.all('SELECT * FROM fornecedores ORDER BY razao_social', [], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/fornecedores', (req, res) => {
|
||||||
|
const { razao_social, telefone, whatsapp, endereco, email } = req.body;
|
||||||
|
const id = uuidv4();
|
||||||
|
|
||||||
|
const query = `INSERT INTO fornecedores (id, razao_social, telefone, whatsapp, endereco, email)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||||
|
|
||||||
|
db.run(query, [id, razao_social, telefone, whatsapp, endereco, email], function(err) {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ id, message: 'Fornecedor cadastrado com sucesso!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === TIPOS DE DESPESAS ===
|
||||||
|
app.get('/api/tipos-despesas', (req, res) => {
|
||||||
|
db.all('SELECT * FROM tipos_despesas ORDER BY nome', [], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/tipos-despesas', (req, res) => {
|
||||||
|
const { nome } = req.body;
|
||||||
|
const id = uuidv4();
|
||||||
|
|
||||||
|
const query = `INSERT INTO tipos_despesas (id, nome) VALUES (?, ?)`;
|
||||||
|
|
||||||
|
db.run(query, [id, nome], function(err) {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ id, message: 'Tipo de despesa criado com sucesso!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === DESPESAS ===
|
||||||
|
app.get('/api/despesas', (req, res) => {
|
||||||
|
const query = `
|
||||||
|
SELECT d.*, td.nome as tipo_nome, f.razao_social as fornecedor_nome
|
||||||
|
FROM despesas d
|
||||||
|
LEFT JOIN tipos_despesas td ON d.tipo_despesa_id = td.id
|
||||||
|
LEFT JOIN fornecedores f ON d.fornecedor_id = f.id
|
||||||
|
ORDER BY d.data DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.all(query, [], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/despesas', (req, res) => {
|
||||||
|
const { tipo_despesa_id, fornecedor_id, data, valor, descricao } = req.body;
|
||||||
|
const id = uuidv4();
|
||||||
|
|
||||||
|
const query = `INSERT INTO despesas (id, tipo_despesa_id, fornecedor_id, data, valor, descricao)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||||
|
|
||||||
|
db.run(query, [id, tipo_despesa_id, fornecedor_id || null, data, valor, descricao], function(err) {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ id, message: 'Despesa cadastrada com sucesso!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/despesas/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { tipo_despesa_id, fornecedor_id, data, valor, descricao } = req.body;
|
||||||
|
|
||||||
|
const query = `UPDATE despesas
|
||||||
|
SET tipo_despesa_id = ?, fornecedor_id = ?, data = ?, valor = ?, descricao = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?`;
|
||||||
|
|
||||||
|
db.run(query, [tipo_despesa_id, fornecedor_id || null, data, valor, descricao, id], function(err) {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ message: 'Despesa atualizada com sucesso!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/despesas/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
db.run('DELETE FROM despesas WHERE id = ?', [id], function(err) {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ message: 'Despesa excluída com sucesso!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === VENDAS ===
|
||||||
|
app.get('/api/vendas', (req, res) => {
|
||||||
|
const query = `
|
||||||
|
SELECT v.*, c.nome_completo as cliente_nome
|
||||||
|
FROM vendas v
|
||||||
|
LEFT JOIN clientes c ON v.cliente_id = c.id
|
||||||
|
ORDER BY v.data_venda DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.all(query, [], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/vendas', (req, res) => {
|
||||||
|
const { cliente_id, tipo_pagamento, valor_total, desconto, parcelas, data_venda, observacoes, itens } = req.body;
|
||||||
|
const vendaId = uuidv4();
|
||||||
|
const valor_parcela = tipo_pagamento === 'parcelado' ? (valor_total - desconto) / parcelas : 0;
|
||||||
|
|
||||||
|
// Iniciar transação
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
|
// Inserir venda
|
||||||
|
const vendaQuery = `INSERT INTO vendas (id, cliente_id, tipo_pagamento, valor_total, desconto, parcelas, valor_parcela, data_venda, observacoes)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||||
|
|
||||||
|
db.run(vendaQuery, [vendaId, cliente_id || null, tipo_pagamento, valor_total, desconto, parcelas, valor_parcela, data_venda, observacoes], function(err) {
|
||||||
|
if (err) {
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserir itens da venda
|
||||||
|
let itemsProcessed = 0;
|
||||||
|
const totalItems = itens.length;
|
||||||
|
|
||||||
|
if (totalItems === 0) {
|
||||||
|
db.run('COMMIT');
|
||||||
|
res.json({ id: vendaId, message: 'Venda registrada com sucesso!' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itens.forEach((item) => {
|
||||||
|
const itemId = uuidv4();
|
||||||
|
const itemQuery = `INSERT INTO venda_itens (id, venda_id, produto_variacao_id, quantidade, valor_unitario, valor_total)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||||
|
|
||||||
|
db.run(itemQuery, [itemId, vendaId, item.produto_variacao_id, item.quantidade, item.valor_unitario, item.valor_total], function(err) {
|
||||||
|
if (err) {
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualizar estoque
|
||||||
|
const updateEstoque = `UPDATE produto_variacoes
|
||||||
|
SET quantidade = quantidade - ?
|
||||||
|
WHERE id = ?`;
|
||||||
|
|
||||||
|
db.run(updateEstoque, [item.quantidade, item.produto_variacao_id], function(err) {
|
||||||
|
if (err) {
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsProcessed++;
|
||||||
|
if (itemsProcessed === totalItems) {
|
||||||
|
db.run('COMMIT');
|
||||||
|
res.json({ id: vendaId, message: 'Venda registrada com sucesso!' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/vendas/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
db.run('DELETE FROM vendas WHERE id = ?', [id], function(err) {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ message: 'Venda excluída com sucesso!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === TESTE ===
|
||||||
|
app.get('/api/test', (req, res) => {
|
||||||
|
res.json({ message: 'API funcionando corretamente!', timestamp: new Date() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// === DASHBOARD ===
|
||||||
|
app.get('/api/dashboard', (req, res) => {
|
||||||
|
const queries = {
|
||||||
|
totalProdutos: 'SELECT COUNT(*) as count FROM produtos',
|
||||||
|
totalClientes: 'SELECT COUNT(*) as count FROM clientes',
|
||||||
|
totalFornecedores: 'SELECT COUNT(*) as count FROM fornecedores',
|
||||||
|
vendasMes: `SELECT COUNT(*) as count, SUM(valor_total) as total
|
||||||
|
FROM vendas
|
||||||
|
WHERE strftime('%Y-%m', data_venda) = strftime('%Y-%m', 'now')`,
|
||||||
|
estoqueTotal: 'SELECT SUM(quantidade) as total FROM produto_variacoes',
|
||||||
|
despesasMes: `SELECT SUM(valor) as total
|
||||||
|
FROM despesas
|
||||||
|
WHERE strftime('%Y-%m', data) = strftime('%Y-%m', 'now')`
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = {};
|
||||||
|
let completed = 0;
|
||||||
|
const total = Object.keys(queries).length;
|
||||||
|
|
||||||
|
Object.entries(queries).forEach(([key, query]) => {
|
||||||
|
db.get(query, [], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
results[key] = { error: err.message };
|
||||||
|
} else {
|
||||||
|
results[key] = row;
|
||||||
|
}
|
||||||
|
|
||||||
|
completed++;
|
||||||
|
if (completed === total) {
|
||||||
|
res.json(results);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Servir arquivos estáticos do React
|
||||||
|
app.get('*', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'client/build', 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Criar diretório de uploads se não existir
|
||||||
|
const fs = require('fs');
|
||||||
|
if (!fs.existsSync('uploads')) {
|
||||||
|
fs.mkdirSync('uploads');
|
||||||
|
}
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Servidor rodando na porta ${PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
163
site/README.md
Normal file
163
site/README.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# 🛍️ Catálogo Web - Liberi Kids
|
||||||
|
|
||||||
|
Catálogo online da **Liberi Kids - Moda Infantil** com carrinho de compras integrado ao WhatsApp.
|
||||||
|
|
||||||
|
## 🎯 Funcionalidades
|
||||||
|
|
||||||
|
### ✅ Catálogo de Produtos
|
||||||
|
- **Carregamento automático** dos produtos cadastrados no sistema
|
||||||
|
- **Filtros inteligentes** por categoria, tamanho e gênero
|
||||||
|
- **Design responsivo** para desktop e mobile
|
||||||
|
- **Imagens otimizadas** com fallback para produtos sem foto
|
||||||
|
|
||||||
|
### 🛒 Carrinho de Compras
|
||||||
|
- **Adicionar/remover produtos** com animações suaves
|
||||||
|
- **Controle de quantidade** individual por item
|
||||||
|
- **Cálculo automático** do total
|
||||||
|
- **Persistência visual** do estado do carrinho
|
||||||
|
|
||||||
|
### 📱 Integração WhatsApp
|
||||||
|
- **Envio automático** do pedido para a vendedora Maiara
|
||||||
|
- **Formatação profissional** da mensagem
|
||||||
|
- **Detalhes completos** do pedido (produtos, quantidades, valores)
|
||||||
|
- **Timestamp** e informações de origem
|
||||||
|
|
||||||
|
### 🎨 Interface Moderna
|
||||||
|
- **Design gradient** com cores atrativas
|
||||||
|
- **Animações CSS** suaves e profissionais
|
||||||
|
- **Tipografia** Google Fonts (Poppins)
|
||||||
|
- **Ícones** Font Awesome
|
||||||
|
- **Layout responsivo** para todos os dispositivos
|
||||||
|
|
||||||
|
## 🚀 Como Usar
|
||||||
|
|
||||||
|
### 1. Acesso ao Catálogo
|
||||||
|
```
|
||||||
|
http://localhost:5000/catalogo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configuração do WhatsApp
|
||||||
|
Edite o arquivo `script.js` e altere o número da vendedora:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const CONFIG = {
|
||||||
|
WHATSAPP_NUMBER: '5511999999999', // Número da Maiara
|
||||||
|
VENDEDORA_NOME: 'Maiara'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Fluxo de Compra
|
||||||
|
1. **Navegar** pelos produtos no catálogo
|
||||||
|
2. **Filtrar** por categoria, tamanho ou gênero
|
||||||
|
3. **Adicionar** produtos ao carrinho
|
||||||
|
4. **Revisar** itens no carrinho lateral
|
||||||
|
5. **Finalizar** pedido via WhatsApp
|
||||||
|
|
||||||
|
## 📋 Estrutura de Arquivos
|
||||||
|
|
||||||
|
```
|
||||||
|
site/
|
||||||
|
├── index.html # Página principal do catálogo
|
||||||
|
├── styles.css # Estilos CSS responsivos
|
||||||
|
├── script.js # JavaScript com todas as funcionalidades
|
||||||
|
└── README.md # Esta documentação
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Integração com o Sistema
|
||||||
|
|
||||||
|
### API Utilizada
|
||||||
|
- **Endpoint:** `/api/catalogo/produtos`
|
||||||
|
- **Método:** GET
|
||||||
|
- **Retorna:** Produtos em estoque formatados para o catálogo
|
||||||
|
|
||||||
|
### Dados dos Produtos
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"nome": "Camiseta Infantil",
|
||||||
|
"preco_venda": 29.90,
|
||||||
|
"tamanho": "M",
|
||||||
|
"genero": "unissex",
|
||||||
|
"estacao": "verao",
|
||||||
|
"categoria": "camiseta",
|
||||||
|
"imagem": "/uploads/produto1.jpg",
|
||||||
|
"estoque": 5,
|
||||||
|
"descricao": "Camiseta confortável..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Personalização
|
||||||
|
|
||||||
|
### Cores do Tema
|
||||||
|
- **Primária:** `#667eea` (Azul gradient)
|
||||||
|
- **Secundária:** `#764ba2` (Roxo gradient)
|
||||||
|
- **Sucesso:** `#10b981` (Verde)
|
||||||
|
- **Erro:** `#ef4444` (Vermelho)
|
||||||
|
- **WhatsApp:** `#25d366` (Verde WhatsApp)
|
||||||
|
|
||||||
|
### Responsividade
|
||||||
|
- **Desktop:** Layout completo com 3-4 colunas
|
||||||
|
- **Tablet:** Layout adaptado com 2-3 colunas
|
||||||
|
- **Mobile:** Layout single-column otimizado
|
||||||
|
|
||||||
|
## 📱 Funcionalidades do WhatsApp
|
||||||
|
|
||||||
|
### Formato da Mensagem
|
||||||
|
```
|
||||||
|
🛍️ NOVO PEDIDO - LIBERI KIDS
|
||||||
|
|
||||||
|
👋 Olá Maiara! Gostaria de fazer um pedido:
|
||||||
|
|
||||||
|
📦 ITENS DO PEDIDO:
|
||||||
|
1. Camiseta Infantil
|
||||||
|
• Tamanho: M
|
||||||
|
• Gênero: Unissex
|
||||||
|
• Quantidade: 2x
|
||||||
|
• Preço unitário: R$ 29,90
|
||||||
|
• Subtotal: R$ 59,80
|
||||||
|
|
||||||
|
📊 RESUMO DO PEDIDO:
|
||||||
|
• Total de itens: 2
|
||||||
|
• Valor total: R$ 59,80
|
||||||
|
|
||||||
|
📱 Pedido feito através do catálogo online
|
||||||
|
🕐 07/10/2024 17:30:15
|
||||||
|
|
||||||
|
Aguardo retorno para confirmar o pedido! 😊
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Sincronização Automática
|
||||||
|
|
||||||
|
### Produtos Novos
|
||||||
|
- **Automático:** Novos produtos aparecem no catálogo imediatamente
|
||||||
|
- **Estoque:** Apenas produtos com estoque > 0 são exibidos
|
||||||
|
- **Ordem:** Produtos mais recentes aparecem primeiro
|
||||||
|
|
||||||
|
### Atualizações em Tempo Real
|
||||||
|
- **Preços:** Atualizados automaticamente
|
||||||
|
- **Estoque:** Produtos sem estoque são ocultados
|
||||||
|
- **Imagens:** Carregamento otimizado com fallback
|
||||||
|
|
||||||
|
## 🛡️ Segurança e Performance
|
||||||
|
|
||||||
|
### Otimizações
|
||||||
|
- **Lazy loading** de imagens
|
||||||
|
- **Debounce** nos filtros
|
||||||
|
- **Cache** de produtos carregados
|
||||||
|
- **Compressão** de imagens
|
||||||
|
|
||||||
|
### Tratamento de Erros
|
||||||
|
- **Fallback** para produtos sem imagem
|
||||||
|
- **Retry** automático em caso de erro de rede
|
||||||
|
- **Mensagens** de erro amigáveis
|
||||||
|
- **Loading states** informativos
|
||||||
|
|
||||||
|
## 📞 Suporte
|
||||||
|
|
||||||
|
Para dúvidas ou problemas com o catálogo:
|
||||||
|
- **WhatsApp:** (11) 99999-9999
|
||||||
|
- **E-mail:** contato@liberikids.com.br
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Liberi Kids - Moda Infantil** 👶✨
|
||||||
BIN
site/assets/Fundo_LiberiKids.jpg
Normal file
BIN
site/assets/Fundo_LiberiKids.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
site/assets/LogoLiberiKids.png
Normal file
BIN
site/assets/LogoLiberiKids.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
129
site/index.html
Normal file
129
site/index.html
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>Liberi Kids - Moda Infantil | Catálogo Online</title>
|
||||||
|
<meta name="description" content="Descubra as melhores roupas infantis na Liberi Kids. Moda moderna, confortável e estilosa para crianças.">
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/png" href="assets/LogoLiberiKids.png">
|
||||||
|
|
||||||
|
<!-- CSS -->
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
|
||||||
|
<!-- Font Awesome para ícones -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<!-- Header Simples -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="assets/LogoLiberiKids.png" alt="Liberi Kids" class="logo-img">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="cart-btn" onclick="toggleCart()">
|
||||||
|
<i class="fas fa-shopping-cart"></i>
|
||||||
|
<span class="cart-count">0</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Filtros Simples -->
|
||||||
|
<section class="filters">
|
||||||
|
<div class="container">
|
||||||
|
<div class="filter-options">
|
||||||
|
<select id="marcaFilter">
|
||||||
|
<option value="">Todas as Marcas</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="tamanhoFilter">
|
||||||
|
<option value="">Todos os Tamanhos</option>
|
||||||
|
<option value="P">P (2-4 anos)</option>
|
||||||
|
<option value="M">M (4-6 anos)</option>
|
||||||
|
<option value="GG">GG (8-10 anos)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="generoFilter">
|
||||||
|
<option value="">Todos</option>
|
||||||
|
<option value="menino">Menino</option>
|
||||||
|
<option value="menina">Menina</option>
|
||||||
|
<option value="unissex">Unissex</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Produtos -->
|
||||||
|
<section id="produtos" class="produtos">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Nossos Produtos</h2>
|
||||||
|
<div class="loading" id="loading">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
<p>Carregando produtos...</p>
|
||||||
|
</div>
|
||||||
|
<div class="produtos-grid" id="produtosGrid">
|
||||||
|
<!-- Produtos serão carregados dinamicamente -->
|
||||||
|
</div>
|
||||||
|
<div class="no-products" id="noProducts" style="display: none;">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
<h3>Nenhum produto encontrado</h3>
|
||||||
|
<p>Tente ajustar os filtros ou volte mais tarde para ver novos produtos.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Carrinho Lateral -->
|
||||||
|
<div class="cart-sidebar" id="cartSidebar">
|
||||||
|
<h3><i class="fas fa-shopping-cart"></i> Seu Carrinho</h3>
|
||||||
|
<button onclick="toggleCart()" class="close-cart">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cart-content" id="cartContent">
|
||||||
|
<div class="empty-cart">
|
||||||
|
<i class="fas fa-shopping-cart"></i>
|
||||||
|
<p>Seu carrinho está vazio</p>
|
||||||
|
<span>Adicione produtos para começar!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cart-footer" id="cartFooter" style="display: none;">
|
||||||
|
<div class="cart-total">
|
||||||
|
<strong>Total: R$ <span id="cartTotal">0,00</span></strong>
|
||||||
|
</div>
|
||||||
|
<button class="checkout-btn" onclick="finalizarPedido()">
|
||||||
|
<i class="fab fa-whatsapp"></i>
|
||||||
|
Finalizar Pedido
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Overlay -->
|
||||||
|
<div class="overlay" id="overlay" onclick="toggleCart()"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- WhatsApp Flutuante -->
|
||||||
|
<div class="whatsapp-float" onclick="abrirWhatsApp()">
|
||||||
|
<i class="fab fa-whatsapp"></i>
|
||||||
|
<div class="whatsapp-tooltip">
|
||||||
|
<strong>Maiara</strong><br>
|
||||||
|
Fale conosco!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript -->
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
site/logo.svg
Normal file
17
site/logo.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
|
||||||
|
<!-- Fundo circular -->
|
||||||
|
<circle cx="200" cy="200" r="180" fill="#f8f9fa" stroke="#333" stroke-width="8"/>
|
||||||
|
|
||||||
|
<!-- Lupa -->
|
||||||
|
<circle cx="200" cy="180" r="120" fill="none" stroke="#333" stroke-width="6"/>
|
||||||
|
<line x1="290" y1="270" x2="340" y2="320" stroke="#8B4513" stroke-width="12" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Texto Liberi -->
|
||||||
|
<text x="200" y="160" text-anchor="middle" font-family="Arial, sans-serif" font-size="36" font-weight="bold" fill="#FF69B4">Liberi</text>
|
||||||
|
|
||||||
|
<!-- Texto KIDS -->
|
||||||
|
<text x="200" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="32" font-weight="bold" fill="#87CEEB">KIDS</text>
|
||||||
|
|
||||||
|
<!-- Texto "moda infantil" -->
|
||||||
|
<text x="200" y="320" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-style="italic" fill="#333">moda infantil</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 895 B |
802
site/script.js
Normal file
802
site/script.js
Normal file
@@ -0,0 +1,802 @@
|
|||||||
|
// Configurações
|
||||||
|
const CONFIG = {
|
||||||
|
API_BASE_URL: 'http://localhost:5000/api',
|
||||||
|
WHATSAPP_NUMBER: '5543999762754', // Número da Maiara
|
||||||
|
VENDEDORA_NOME: 'Maiara',
|
||||||
|
EVOLUTION_API: {
|
||||||
|
BASE_URL: 'https://criadordigital-evolution.jesdfs.easypanel.host',
|
||||||
|
INSTANCE_NAME: 'Tiago',
|
||||||
|
API_KEY: 'DBDF609168B1-48A3-8A4A-5E50D0300F2C'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estado global
|
||||||
|
let produtos = [];
|
||||||
|
let carrinho = [];
|
||||||
|
let filtros = {
|
||||||
|
marca: '',
|
||||||
|
tamanho: '',
|
||||||
|
genero: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inicialização
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
carregarProdutos();
|
||||||
|
inicializarEventListeners();
|
||||||
|
atualizarContadorCarrinho();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
function inicializarEventListeners() {
|
||||||
|
// Filtros
|
||||||
|
document.getElementById('marcaFilter').addEventListener('change', aplicarFiltros);
|
||||||
|
document.getElementById('tamanhoFilter').addEventListener('change', aplicarFiltros);
|
||||||
|
document.getElementById('generoFilter').addEventListener('change', aplicarFiltros);
|
||||||
|
|
||||||
|
// Smooth scroll para links de navegação
|
||||||
|
document.querySelectorAll('a[href^="#"]').forEach(link => {
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const target = document.querySelector(this.getAttribute('href'));
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para carregar produtos do banco de dados
|
||||||
|
async function carregarProdutos() {
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
const grid = document.getElementById('produtosGrid');
|
||||||
|
const noProducts = document.getElementById('noProducts');
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.style.display = 'block';
|
||||||
|
|
||||||
|
// Buscar produtos da API
|
||||||
|
const response = await fetch(`${CONFIG.API_BASE_URL}/catalogo/produtos`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Erro ao buscar produtos');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Dados recebidos da API:', data); // Debug
|
||||||
|
produtos = data.data || data || [];
|
||||||
|
|
||||||
|
loading.style.display = 'none';
|
||||||
|
|
||||||
|
if (produtos.length === 0) {
|
||||||
|
noProducts.style.display = 'block';
|
||||||
|
grid.innerHTML = '';
|
||||||
|
} else {
|
||||||
|
noProducts.style.display = 'none';
|
||||||
|
popularFiltros();
|
||||||
|
renderizarProdutos();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar produtos:', error);
|
||||||
|
loading.style.display = 'none';
|
||||||
|
noProducts.style.display = 'block';
|
||||||
|
grid.innerHTML = '';
|
||||||
|
} finally {
|
||||||
|
mostrarLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mostrar/ocultar loading
|
||||||
|
function mostrarLoading(show) {
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
const grid = document.getElementById('produtosGrid');
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
loading.style.display = 'block';
|
||||||
|
grid.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
loading.style.display = 'none';
|
||||||
|
grid.style.display = 'grid';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar erro
|
||||||
|
function mostrarErro(mensagem) {
|
||||||
|
const grid = document.getElementById('produtosGrid');
|
||||||
|
grid.innerHTML = `
|
||||||
|
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem; color: #dc3545;">
|
||||||
|
<i class="fas fa-exclamation-triangle" style="font-size: 3rem; margin-bottom: 1rem;"></i>
|
||||||
|
<h3>Ops! Algo deu errado</h3>
|
||||||
|
<p>${mensagem}</p>
|
||||||
|
<button onclick="carregarProdutos()" style="margin-top: 1rem; padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer;">
|
||||||
|
Tentar Novamente
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderizar produtos
|
||||||
|
function renderizarProdutos(produtosList = produtos) {
|
||||||
|
const grid = document.getElementById('produtosGrid');
|
||||||
|
const noProducts = document.getElementById('noProducts');
|
||||||
|
|
||||||
|
console.log('Renderizando produtos:', produtosList); // Debug
|
||||||
|
|
||||||
|
if (!produtosList || produtosList.length === 0) {
|
||||||
|
grid.style.display = 'none';
|
||||||
|
noProducts.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.style.display = 'grid';
|
||||||
|
noProducts.style.display = 'none';
|
||||||
|
|
||||||
|
grid.innerHTML = produtosList.map(produto => {
|
||||||
|
console.log('Renderizando produto individual:', produto); // Debug
|
||||||
|
|
||||||
|
// Pegar a primeira variação disponível para mostrar
|
||||||
|
const variacao = produto.variacoes && produto.variacoes.length > 0 ? produto.variacoes[0] : null;
|
||||||
|
const temEstoque = produto.estoque_total > 0 || (variacao && variacao.quantidade > 0);
|
||||||
|
const preco = produto.valor_revenda || produto.preco_venda || 0;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="produto-card" data-id="${produto.id || ''}">
|
||||||
|
<div class="produto-image">
|
||||||
|
${produto.foto_url ?
|
||||||
|
`<img src="http://localhost:5000${produto.foto_url}" alt="${produto.nome || 'Produto'}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||||
|
<div class="placeholder-img" style="display: none;"><i class="fas fa-tshirt"></i></div>` :
|
||||||
|
variacao && variacao.foto_url ?
|
||||||
|
`<img src="http://localhost:5000${variacao.foto_url}" alt="${produto.nome || 'Produto'}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||||
|
<div class="placeholder-img" style="display: none;"><i class="fas fa-tshirt"></i></div>` :
|
||||||
|
`<div class="placeholder-img"><i class="fas fa-tshirt"></i></div>`
|
||||||
|
}
|
||||||
|
${!temEstoque ? '<div class="produto-sem-estoque">Sem Estoque</div>' : ''}
|
||||||
|
</div>
|
||||||
|
<div class="produto-info">
|
||||||
|
<div class="produto-header">
|
||||||
|
<h3 class="produto-nome">${produto.marca ? produto.marca + ' - ' : ''}${produto.nome || 'Produto sem nome'}</h3>
|
||||||
|
${produto.id_produto ? `<span class="produto-codigo">Cód: ${produto.id_produto}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="produto-badges">
|
||||||
|
${variacao && variacao.tamanho ? `<span class="produto-badge badge-tamanho">${variacao.tamanho}</span>` : ''}
|
||||||
|
${variacao && variacao.cor ? `<span class="produto-badge badge-cor" style="background-color: ${getCorBadge(variacao.cor)}">${variacao.cor}</span>` : ''}
|
||||||
|
${produto.genero ? `<span class="produto-badge badge-genero">${formatarGenero(produto.genero)}</span>` : ''}
|
||||||
|
${produto.estacao ? `<span class="produto-badge badge-estacao">${formatarEstacao(produto.estacao)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="produto-detalhes">
|
||||||
|
${produto.fornecedor ? `<small>Fornecedor: ${produto.fornecedor}</small>` : ''}
|
||||||
|
${produto.estoque_total ? `<small>Estoque: ${produto.estoque_total} unidades</small>` : ''}
|
||||||
|
${variacao && !produto.estoque_total ? `<small>Estoque: ${variacao.quantidade || 0} unidades</small>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="produto-preco">R$ ${formatarPreco(preco)}</div>
|
||||||
|
|
||||||
|
<button class="add-to-cart" onclick="adicionarAoCarrinho(${produto.id || 0})" ${!temEstoque ? 'disabled' : ''}>
|
||||||
|
<i class="fas fa-cart-plus"></i> ${temEstoque ? 'Adicionar ao Carrinho' : 'Sem Estoque'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatadores
|
||||||
|
function formatarPreco(preco) {
|
||||||
|
return parseFloat(preco || 0).toLocaleString('pt-BR', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatarGenero(genero) {
|
||||||
|
const generos = {
|
||||||
|
'masculino': 'Menino',
|
||||||
|
'feminino': 'Menina',
|
||||||
|
'unissex': 'Unissex'
|
||||||
|
};
|
||||||
|
return generos[genero] || genero || 'Unissex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatarEstacao(estacao) {
|
||||||
|
const estacoes = {
|
||||||
|
'verao': 'Verão',
|
||||||
|
'inverno': 'Inverno',
|
||||||
|
'outono': 'Outono',
|
||||||
|
'primavera': 'Primavera'
|
||||||
|
};
|
||||||
|
return estacoes[estacao] || estacao || 'Meia Estação';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCorBadge(cor) {
|
||||||
|
const cores = {
|
||||||
|
'azul': '#c0daf3',
|
||||||
|
'rosa': '#ecbad7',
|
||||||
|
'amarelo': '#f9f295',
|
||||||
|
'verde': '#90ee90',
|
||||||
|
'vermelho': '#ffb3ba',
|
||||||
|
'branco': '#f6f4e6',
|
||||||
|
'preto': '#464444'
|
||||||
|
};
|
||||||
|
return cores[cor?.toLowerCase()] || '#ddd9c7';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatarGenero(genero) {
|
||||||
|
const generos = {
|
||||||
|
'menino': 'Menino',
|
||||||
|
'menina': 'Menina',
|
||||||
|
'unissex': 'Unissex'
|
||||||
|
};
|
||||||
|
return generos[genero] || 'Unissex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatarEstacao(estacao) {
|
||||||
|
const estacoes = {
|
||||||
|
'verao': 'Verão',
|
||||||
|
'inverno': 'Inverno',
|
||||||
|
'meia-estacao': 'Meia Estação'
|
||||||
|
};
|
||||||
|
return estacoes[estacao] || 'Meia Estação';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtros
|
||||||
|
function aplicarFiltros() {
|
||||||
|
filtros.categoria = document.getElementById('categoriaFilter').value;
|
||||||
|
filtros.tamanho = document.getElementById('tamanhoFilter').value;
|
||||||
|
filtros.genero = document.getElementById('generoFilter').value;
|
||||||
|
|
||||||
|
let produtosFiltrados = produtos.filter(produto => {
|
||||||
|
const matchCategoria = !filtros.categoria ||
|
||||||
|
produto.nome.toLowerCase().includes(filtros.categoria.toLowerCase()) ||
|
||||||
|
(produto.categoria && produto.categoria.toLowerCase().includes(filtros.categoria.toLowerCase()));
|
||||||
|
|
||||||
|
const matchTamanho = !filtros.tamanho || produto.tamanho === filtros.tamanho;
|
||||||
|
const matchGenero = !filtros.genero || produto.genero === filtros.genero;
|
||||||
|
|
||||||
|
return matchCategoria && matchTamanho && matchGenero;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderizarProdutos(produtosFiltrados);
|
||||||
|
}
|
||||||
|
|
||||||
|
function limparFiltros() {
|
||||||
|
document.getElementById('categoriaFilter').value = '';
|
||||||
|
document.getElementById('tamanhoFilter').value = '';
|
||||||
|
document.getElementById('generoFilter').value = '';
|
||||||
|
|
||||||
|
filtros = { categoria: '', tamanho: '', genero: '' };
|
||||||
|
renderizarProdutos(produtos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carrinho de Compras
|
||||||
|
function adicionarAoCarrinho(produtoId) {
|
||||||
|
const produto = produtos.find(p => p.id === produtoId);
|
||||||
|
if (!produto) return;
|
||||||
|
|
||||||
|
const itemExistente = carrinho.find(item => item.id === produtoId);
|
||||||
|
|
||||||
|
if (itemExistente) {
|
||||||
|
itemExistente.quantidade += 1;
|
||||||
|
} else {
|
||||||
|
carrinho.push({
|
||||||
|
id: produto.id,
|
||||||
|
nome: produto.nome,
|
||||||
|
preco: parseFloat(produto.preco_venda),
|
||||||
|
tamanho: produto.tamanho,
|
||||||
|
genero: produto.genero,
|
||||||
|
imagem: produto.imagem,
|
||||||
|
quantidade: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
atualizarCarrinho();
|
||||||
|
mostrarNotificacao('Produto adicionado ao carrinho!', 'success');
|
||||||
|
|
||||||
|
// Animação do botão
|
||||||
|
const btn = event.target.closest('.add-to-cart');
|
||||||
|
btn.style.transform = 'scale(0.95)';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.style.transform = 'scale(1)';
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removerDoCarrinho(produtoId) {
|
||||||
|
carrinho = carrinho.filter(item => item.id !== produtoId);
|
||||||
|
atualizarCarrinho();
|
||||||
|
mostrarNotificacao('Produto removido do carrinho', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
function alterarQuantidade(produtoId, novaQuantidade) {
|
||||||
|
if (novaQuantidade <= 0) {
|
||||||
|
removerDoCarrinho(produtoId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = carrinho.find(item => item.id === produtoId);
|
||||||
|
if (item) {
|
||||||
|
item.quantidade = novaQuantidade;
|
||||||
|
atualizarCarrinho();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function atualizarCarrinho() {
|
||||||
|
atualizarContadorCarrinho();
|
||||||
|
renderizarCarrinho();
|
||||||
|
atualizarTotalCarrinho();
|
||||||
|
}
|
||||||
|
|
||||||
|
function atualizarContadorCarrinho() {
|
||||||
|
const contador = document.querySelector('.cart-count');
|
||||||
|
const totalItens = carrinho.reduce((total, item) => total + item.quantidade, 0);
|
||||||
|
contador.textContent = totalItens;
|
||||||
|
|
||||||
|
// Animação do contador
|
||||||
|
if (totalItens > 0) {
|
||||||
|
contador.style.display = 'flex';
|
||||||
|
contador.style.animation = 'pulse 0.3s ease';
|
||||||
|
} else {
|
||||||
|
contador.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderizarCarrinho() {
|
||||||
|
const cartContent = document.getElementById('cartContent');
|
||||||
|
const cartFooter = document.getElementById('cartFooter');
|
||||||
|
|
||||||
|
if (carrinho.length === 0) {
|
||||||
|
cartContent.innerHTML = `
|
||||||
|
<div class="empty-cart">
|
||||||
|
<i class="fas fa-shopping-cart"></i>
|
||||||
|
<p>Seu carrinho está vazio</p>
|
||||||
|
<span>Adicione produtos para começar!</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
cartFooter.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cartContent.innerHTML = carrinho.map(item => `
|
||||||
|
<div class="cart-item">
|
||||||
|
<div class="cart-item-image">
|
||||||
|
${item.imagem ?
|
||||||
|
`<img src="${item.imagem}" alt="${item.nome}" style="width: 100%; height: 100%; object-fit: cover; border-radius: 8px;">` :
|
||||||
|
`<i class="fas fa-tshirt"></i>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="cart-item-info">
|
||||||
|
<div class="cart-item-name">${item.nome}</div>
|
||||||
|
<div class="cart-item-details">${item.tamanho} • ${formatarGenero(item.genero)}</div>
|
||||||
|
<div class="cart-item-price">R$ ${formatarPreco(item.preco)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="cart-item-actions">
|
||||||
|
<button class="qty-btn" onclick="alterarQuantidade(${item.id}, ${item.quantidade - 1})">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
<span style="margin: 0 0.5rem; font-weight: 600;">${item.quantidade}</span>
|
||||||
|
<button class="qty-btn" onclick="alterarQuantidade(${item.id}, ${item.quantidade + 1})">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
<button class="remove-item" onclick="removerDoCarrinho(${item.id})">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
cartFooter.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function atualizarTotalCarrinho() {
|
||||||
|
const total = carrinho.reduce((sum, item) => sum + (item.preco * item.quantidade), 0);
|
||||||
|
const totalElement = document.getElementById('cartTotal');
|
||||||
|
if (totalElement) {
|
||||||
|
totalElement.textContent = formatarPreco(total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCart() {
|
||||||
|
const sidebar = document.getElementById('cartSidebar');
|
||||||
|
const overlay = document.getElementById('overlay');
|
||||||
|
|
||||||
|
sidebar.classList.toggle('active');
|
||||||
|
overlay.classList.toggle('active');
|
||||||
|
|
||||||
|
if (sidebar.classList.contains('active')) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalizar Pedido - WhatsApp
|
||||||
|
function finalizarPedido() {
|
||||||
|
if (carrinho.length === 0) {
|
||||||
|
mostrarNotificacao('Seu carrinho está vazio!', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = carrinho.reduce((sum, item) => sum + (item.preco * item.quantidade), 0);
|
||||||
|
const totalItens = carrinho.reduce((sum, item) => sum + item.quantidade, 0);
|
||||||
|
|
||||||
|
let mensagem = `🛍️ *NOVO PEDIDO - LIBERI KIDS*\n\n`;
|
||||||
|
mensagem += `👋 Olá ${CONFIG.VENDEDORA_NOME}! Gostaria de fazer um pedido:\n\n`;
|
||||||
|
|
||||||
|
mensagem += `📦 *ITENS DO PEDIDO:*\n`;
|
||||||
|
carrinho.forEach((item, index) => {
|
||||||
|
mensagem += `${index + 1}. *${item.nome}*\n`;
|
||||||
|
mensagem += ` • Tamanho: ${item.tamanho}\n`;
|
||||||
|
mensagem += ` • Gênero: ${formatarGenero(item.genero)}\n`;
|
||||||
|
mensagem += ` • Quantidade: ${item.quantidade}x\n`;
|
||||||
|
mensagem += ` • Preço unitário: R$ ${formatarPreco(item.preco)}\n`;
|
||||||
|
mensagem += ` • Subtotal: R$ ${formatarPreco(item.preco * item.quantidade)}\n\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
mensagem += `📊 *RESUMO DO PEDIDO:*\n`;
|
||||||
|
mensagem += `• Total de itens: ${totalItens}\n`;
|
||||||
|
mensagem += `• *Valor total: R$ ${formatarPreco(total)}*\n\n`;
|
||||||
|
|
||||||
|
mensagem += `📱 Pedido feito através do catálogo online da Liberi Kids\n`;
|
||||||
|
mensagem += `🕐 ${new Date().toLocaleString('pt-BR')}\n\n`;
|
||||||
|
|
||||||
|
mensagem += `Aguardo retorno para confirmar o pedido e combinar a entrega! 😊`;
|
||||||
|
|
||||||
|
// Codificar mensagem para URL
|
||||||
|
const mensagemCodificada = encodeURIComponent(mensagem);
|
||||||
|
const urlWhatsApp = `https://wa.me/${CONFIG.WHATSAPP_NUMBER}?text=${mensagemCodificada}`;
|
||||||
|
|
||||||
|
// Abrir WhatsApp
|
||||||
|
window.open(urlWhatsApp, '_blank');
|
||||||
|
|
||||||
|
// Limpar carrinho após envio
|
||||||
|
setTimeout(() => {
|
||||||
|
if (confirm('Pedido enviado! Deseja limpar o carrinho?')) {
|
||||||
|
carrinho = [];
|
||||||
|
atualizarCarrinho();
|
||||||
|
toggleCart();
|
||||||
|
mostrarNotificacao('Pedido enviado com sucesso!', 'success');
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notificações
|
||||||
|
function mostrarNotificacao(mensagem, tipo = 'info') {
|
||||||
|
// Remover notificação existente
|
||||||
|
const existente = document.querySelector('.notification');
|
||||||
|
if (existente) {
|
||||||
|
existente.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification notification-${tipo}`;
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
|
<i class="fas fa-${getIconeNotificacao(tipo)}"></i>
|
||||||
|
<span>${mensagem}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Estilos da notificação
|
||||||
|
Object.assign(notification.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '100px',
|
||||||
|
right: '20px',
|
||||||
|
background: getCorNotificacao(tipo),
|
||||||
|
color: 'white',
|
||||||
|
padding: '1rem 1.5rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||||
|
zIndex: '3000',
|
||||||
|
animation: 'slideInRight 0.3s ease',
|
||||||
|
maxWidth: '300px'
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Remover após 3 segundos
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideOutRight 0.3s ease';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconeNotificacao(tipo) {
|
||||||
|
const icones = {
|
||||||
|
'success': 'check-circle',
|
||||||
|
'error': 'exclamation-circle',
|
||||||
|
'warning': 'exclamation-triangle',
|
||||||
|
'info': 'info-circle'
|
||||||
|
};
|
||||||
|
return icones[tipo] || 'info-circle';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCorNotificacao(tipo) {
|
||||||
|
const cores = {
|
||||||
|
'success': '#10b981',
|
||||||
|
'error': '#ef4444',
|
||||||
|
'warning': '#f59e0b',
|
||||||
|
'info': '#3b82f6'
|
||||||
|
};
|
||||||
|
return cores[tipo] || '#3b82f6';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adicionar animações CSS dinamicamente
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOutRight {
|
||||||
|
from { transform: translateX(0); opacity: 1; }
|
||||||
|
to { transform: translateX(100%); opacity: 0; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// Fechar carrinho ao clicar fora
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
const sidebar = document.getElementById('cartSidebar');
|
||||||
|
const cartBtn = document.querySelector('.cart-btn');
|
||||||
|
|
||||||
|
if (sidebar.classList.contains('active') &&
|
||||||
|
!sidebar.contains(e.target) &&
|
||||||
|
!cartBtn.contains(e.target)) {
|
||||||
|
toggleCart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll suave para seções
|
||||||
|
function scrollToSection(sectionId) {
|
||||||
|
const section = document.getElementById(sectionId);
|
||||||
|
if (section) {
|
||||||
|
section.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para abrir WhatsApp
|
||||||
|
async function abrirWhatsApp() {
|
||||||
|
const numero = CONFIG.WHATSAPP_NUMBER;
|
||||||
|
const nome = CONFIG.VENDEDORA_NOME;
|
||||||
|
const mensagem = `Olá ${nome}! Vim através do catálogo online da Liberi Kids e gostaria de saber mais sobre os produtos. Pode me ajudar?`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Tentar enviar via Evolution API primeiro
|
||||||
|
await enviarMensagemEvolution(mensagem);
|
||||||
|
mostrarNotificacao('Mensagem enviada via WhatsApp!');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao enviar via Evolution API:', error);
|
||||||
|
|
||||||
|
// Fallback para WhatsApp Web
|
||||||
|
const mensagemCodificada = encodeURIComponent(mensagem);
|
||||||
|
const urlWhatsApp = `https://wa.me/${numero}?text=${mensagemCodificada}`;
|
||||||
|
window.open(urlWhatsApp, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popular filtros dinamicamente
|
||||||
|
function popularFiltros() {
|
||||||
|
// Marcas únicas
|
||||||
|
const marcas = [...new Set(produtos.map(p => p.marca).filter(Boolean))];
|
||||||
|
const marcaSelect = document.getElementById('marcaFilter');
|
||||||
|
marcaSelect.innerHTML = '<option value="">Todas as Marcas</option>';
|
||||||
|
marcas.forEach(marca => {
|
||||||
|
marcaSelect.innerHTML += `<option value="${marca}">${marca}</option>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tamanhos únicos das variações
|
||||||
|
const tamanhos = [...new Set(produtos.flatMap(p =>
|
||||||
|
p.variacoes?.map(v => v.tamanho) || []
|
||||||
|
).filter(Boolean))];
|
||||||
|
const tamanhoSelect = document.getElementById('tamanhoFilter');
|
||||||
|
tamanhoSelect.innerHTML = '<option value="">Todos os Tamanhos</option>';
|
||||||
|
tamanhos.forEach(tamanho => {
|
||||||
|
tamanhoSelect.innerHTML += `<option value="${tamanho}">${tamanho}</option>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gêneros únicos
|
||||||
|
const generos = [...new Set(produtos.map(p => p.genero).filter(Boolean))];
|
||||||
|
const generoSelect = document.getElementById('generoFilter');
|
||||||
|
generoSelect.innerHTML = '<option value="">Todos</option>';
|
||||||
|
generos.forEach(genero => {
|
||||||
|
generoSelect.innerHTML += `<option value="${genero}">${formatarGenero(genero)}</option>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar filtros
|
||||||
|
function aplicarFiltros() {
|
||||||
|
filtros.marca = document.getElementById('marcaFilter').value;
|
||||||
|
filtros.tamanho = document.getElementById('tamanhoFilter').value;
|
||||||
|
filtros.genero = document.getElementById('generoFilter').value;
|
||||||
|
|
||||||
|
const produtosFiltrados = produtos.filter(produto => {
|
||||||
|
// Filtro por marca
|
||||||
|
if (filtros.marca && produto.marca !== filtros.marca) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro por gênero
|
||||||
|
if (filtros.genero && produto.genero !== filtros.genero) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro por tamanho (verifica nas variações)
|
||||||
|
if (filtros.tamanho) {
|
||||||
|
const temTamanho = produto.variacoes?.some(v => v.tamanho === filtros.tamanho);
|
||||||
|
if (!temTamanho) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderizarProdutos(produtosFiltrados);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funções do carrinho
|
||||||
|
function adicionarAoCarrinho(produtoId) {
|
||||||
|
const produto = produtos.find(p => p.id === produtoId);
|
||||||
|
if (!produto) return;
|
||||||
|
|
||||||
|
const itemExistente = carrinho.find(item => item.id === produtoId);
|
||||||
|
|
||||||
|
if (itemExistente) {
|
||||||
|
itemExistente.quantidade++;
|
||||||
|
} else {
|
||||||
|
carrinho.push({
|
||||||
|
id: produto.id,
|
||||||
|
nome: `${produto.marca} - ${produto.nome}`,
|
||||||
|
preco: produto.valor_revenda,
|
||||||
|
quantidade: 1,
|
||||||
|
foto: produto.foto_url || (produto.variacoes?.[0]?.foto_url)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
atualizarContadorCarrinho();
|
||||||
|
mostrarNotificacao('Produto adicionado ao carrinho!');
|
||||||
|
}
|
||||||
|
|
||||||
|
function atualizarContadorCarrinho() {
|
||||||
|
const contador = document.querySelector('.cart-count');
|
||||||
|
const totalItens = carrinho.reduce((total, item) => total + item.quantidade, 0);
|
||||||
|
contador.textContent = totalItens;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mostrarNotificacao(mensagem) {
|
||||||
|
const notificacao = document.createElement('div');
|
||||||
|
notificacao.className = 'notificacao';
|
||||||
|
notificacao.textContent = mensagem;
|
||||||
|
notificacao.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #25d366;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
z-index: 9999;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notificacao);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notificacao.remove();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCart() {
|
||||||
|
const cartSidebar = document.getElementById('cartSidebar');
|
||||||
|
const overlay = document.getElementById('overlay');
|
||||||
|
|
||||||
|
cartSidebar.classList.toggle('active');
|
||||||
|
overlay.classList.toggle('active');
|
||||||
|
|
||||||
|
if (cartSidebar.classList.contains('active')) {
|
||||||
|
renderizarCarrinho();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderizarCarrinho() {
|
||||||
|
const cartContent = document.querySelector('.cart-content');
|
||||||
|
const cartTotal = document.querySelector('.cart-total');
|
||||||
|
|
||||||
|
if (carrinho.length === 0) {
|
||||||
|
cartContent.innerHTML = `
|
||||||
|
<div style="text-align: center; padding: 2rem; color: #666;">
|
||||||
|
<i class="fas fa-shopping-cart" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;"></i>
|
||||||
|
<p>Seu carrinho está vazio</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
cartTotal.innerHTML = '<strong>Total: R$ 0,00</strong>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = carrinho.reduce((sum, item) => sum + (item.preco * item.quantidade), 0);
|
||||||
|
|
||||||
|
cartContent.innerHTML = carrinho.map(item => `
|
||||||
|
<div class="cart-item">
|
||||||
|
<div class="item-info">
|
||||||
|
<h4>${item.nome}</h4>
|
||||||
|
<p>R$ ${formatarPreco(item.preco)} x ${item.quantidade}</p>
|
||||||
|
</div>
|
||||||
|
<button class="remove-item" onclick="removerDoCarrinho('${item.id}')">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
cartTotal.innerHTML = `<strong>Total: R$ ${formatarPreco(total)}</strong>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removerDoCarrinho(produtoId) {
|
||||||
|
const index = carrinho.findIndex(item => item.id === produtoId);
|
||||||
|
if (index > -1) {
|
||||||
|
carrinho.splice(index, 1);
|
||||||
|
atualizarContadorCarrinho();
|
||||||
|
renderizarCarrinho();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function finalizarPedido() {
|
||||||
|
if (carrinho.length === 0) {
|
||||||
|
mostrarNotificacao('Adicione produtos ao carrinho primeiro!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = carrinho.reduce((sum, item) => sum + (item.preco * item.quantidade), 0);
|
||||||
|
const itens = carrinho.map(item => `${item.quantidade}x ${item.nome} - R$ ${formatarPreco(item.preco)}`).join('\n');
|
||||||
|
|
||||||
|
const mensagem = `🛍️ *Pedido Liberi Kids*\n\n${itens}\n\n💰 *Total: R$ ${formatarPreco(total)}*\n\nGostaria de finalizar este pedido!`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Tentar enviar via Evolution API primeiro
|
||||||
|
await enviarMensagemEvolution(mensagem);
|
||||||
|
mostrarNotificacao('Pedido enviado via WhatsApp!');
|
||||||
|
|
||||||
|
// Limpar carrinho após envio
|
||||||
|
carrinho = [];
|
||||||
|
atualizarContadorCarrinho();
|
||||||
|
renderizarCarrinho();
|
||||||
|
toggleCart();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao enviar via Evolution API:', error);
|
||||||
|
|
||||||
|
// Fallback para WhatsApp Web
|
||||||
|
const mensagemCodificada = encodeURIComponent(mensagem);
|
||||||
|
const urlWhatsApp = `https://wa.me/${CONFIG.WHATSAPP_NUMBER}?text=${mensagemCodificada}`;
|
||||||
|
window.open(urlWhatsApp, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enviarMensagemEvolution(mensagem) {
|
||||||
|
const response = await fetch(`${CONFIG.EVOLUTION_API.BASE_URL}/message/sendText/${CONFIG.EVOLUTION_API.INSTANCE_NAME}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'apikey': CONFIG.EVOLUTION_API.API_KEY
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
number: CONFIG.WHATSAPP_NUMBER,
|
||||||
|
text: mensagem
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Falha ao enviar mensagem via Evolution API');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
1033
site/styles.css
Normal file
1033
site/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
15
sql/add-description-field.sql
Normal file
15
sql/add-description-field.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ADICIONAR CAMPO DESCRIÇÃO NA TABELA PRODUTOS
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Adicionar campo descrição na tabela produtos
|
||||||
|
ALTER TABLE produtos ADD COLUMN IF NOT EXISTS descricao TEXT;
|
||||||
|
|
||||||
|
-- Comentário para documentar o campo
|
||||||
|
COMMENT ON COLUMN produtos.descricao IS 'Descrição detalhada do produto, pode ser gerada automaticamente via ChatGPT';
|
||||||
|
|
||||||
|
-- Verificar se o campo foi adicionado
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'produtos'
|
||||||
|
AND column_name = 'descricao';
|
||||||
22
sql/add-id-cliente-column.sql
Normal file
22
sql/add-id-cliente-column.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- Adicionar coluna id_cliente na tabela clientes (numérico)
|
||||||
|
ALTER TABLE clientes ADD COLUMN IF NOT EXISTS id_cliente VARCHAR(10);
|
||||||
|
|
||||||
|
-- Criar índice para melhor performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_clientes_id_cliente ON clientes(id_cliente);
|
||||||
|
|
||||||
|
-- Gerar IDs numéricos sequenciais para clientes existentes (se houver)
|
||||||
|
WITH numbered_clients AS (
|
||||||
|
SELECT id, ROW_NUMBER() OVER (ORDER BY created_at) as row_num
|
||||||
|
FROM clientes
|
||||||
|
WHERE id_cliente IS NULL
|
||||||
|
)
|
||||||
|
UPDATE clientes
|
||||||
|
SET id_cliente = numbered_clients.row_num::TEXT
|
||||||
|
FROM numbered_clients
|
||||||
|
WHERE clientes.id = numbered_clients.id;
|
||||||
|
|
||||||
|
-- Tornar a coluna NOT NULL após popular os dados existentes
|
||||||
|
ALTER TABLE clientes ALTER COLUMN id_cliente SET NOT NULL;
|
||||||
|
|
||||||
|
-- Adicionar constraint de unicidade
|
||||||
|
ALTER TABLE clientes ADD CONSTRAINT unique_id_cliente UNIQUE (id_cliente);
|
||||||
13
sql/add-id-venda-column.sql
Normal file
13
sql/add-id-venda-column.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
-- Adicionar coluna id_venda na tabela vendas
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS id_venda VARCHAR(20);
|
||||||
|
|
||||||
|
-- Adicionar coluna data_primeiro_vencimento na tabela vendas
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS data_primeiro_vencimento DATE;
|
||||||
|
|
||||||
|
-- Criar índice para busca rápida por ID da venda
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vendas_id_venda ON vendas(id_venda);
|
||||||
|
|
||||||
|
-- Atualizar vendas existentes com ID gerado (usando created_at se existir)
|
||||||
|
UPDATE vendas
|
||||||
|
SET id_venda = 'VD' || TO_CHAR(COALESCE(created_at, NOW()), 'YYYYMMDDHH24MISS')
|
||||||
|
WHERE id_venda IS NULL;
|
||||||
17
sql/add-pix-fields.sql
Normal file
17
sql/add-pix-fields.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
-- Adicionar campos PIX na tabela vendas
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS status_pagamento VARCHAR(20) DEFAULT 'pendente';
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS data_pagamento TIMESTAMP;
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS pix_payment_id VARCHAR(100);
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS pix_qr_code TEXT;
|
||||||
|
ALTER TABLE vendas ADD COLUMN IF NOT EXISTS metodo_pagamento VARCHAR(20) DEFAULT 'dinheiro';
|
||||||
|
|
||||||
|
-- Criar índices para performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vendas_status_pagamento ON vendas(status_pagamento);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vendas_pix_payment_id ON vendas(pix_payment_id);
|
||||||
|
|
||||||
|
-- Comentários para documentação
|
||||||
|
COMMENT ON COLUMN vendas.status_pagamento IS 'Status do pagamento: pendente, pago, cancelado, expirado';
|
||||||
|
COMMENT ON COLUMN vendas.data_pagamento IS 'Data e hora da confirmação do pagamento';
|
||||||
|
COMMENT ON COLUMN vendas.pix_payment_id IS 'ID do pagamento no Mercado Pago';
|
||||||
|
COMMENT ON COLUMN vendas.pix_qr_code IS 'Código PIX para copiar e colar';
|
||||||
|
COMMENT ON COLUMN vendas.metodo_pagamento IS 'Método: dinheiro, cartao, pix, transferencia';
|
||||||
11
sql/add-valor-custo-produtos.sql
Normal file
11
sql/add-valor-custo-produtos.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- Adicionar coluna valor_custo na tabela produtos para controle de custos
|
||||||
|
ALTER TABLE produtos ADD COLUMN IF NOT EXISTS valor_custo DECIMAL(10,2) DEFAULT 0;
|
||||||
|
|
||||||
|
-- Comentário para documentação
|
||||||
|
COMMENT ON COLUMN produtos.valor_custo IS 'Valor de custo do produto para cálculo de lucro real';
|
||||||
|
|
||||||
|
-- Atualizar produtos existentes com valor de custo baseado no valor de compra (se existir)
|
||||||
|
-- Se não existir valor_compra, usar 60% do valor_revenda como estimativa
|
||||||
|
UPDATE produtos
|
||||||
|
SET valor_custo = COALESCE(valor_compra, valor_revenda * 0.6)
|
||||||
|
WHERE valor_custo = 0 OR valor_custo IS NULL;
|
||||||
37
sql/alter-despesas-text-fields.sql
Normal file
37
sql/alter-despesas-text-fields.sql
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ALTERAÇÃO DA TABELA DESPESAS PARA CAMPOS LIVRES
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Adicionar novos campos de texto livre
|
||||||
|
ALTER TABLE despesas ADD COLUMN IF NOT EXISTS tipo_nome TEXT;
|
||||||
|
ALTER TABLE despesas ADD COLUMN IF NOT EXISTS fornecedor_nome TEXT;
|
||||||
|
|
||||||
|
-- Migrar dados existentes (se houver)
|
||||||
|
UPDATE despesas
|
||||||
|
SET tipo_nome = (
|
||||||
|
SELECT nome FROM tipos_despesas
|
||||||
|
WHERE tipos_despesas.id = despesas.tipo_despesa_id
|
||||||
|
)
|
||||||
|
WHERE tipo_despesa_id IS NOT NULL AND tipo_nome IS NULL;
|
||||||
|
|
||||||
|
UPDATE despesas
|
||||||
|
SET fornecedor_nome = (
|
||||||
|
SELECT razao_social FROM fornecedores
|
||||||
|
WHERE fornecedores.id = despesas.fornecedor_id
|
||||||
|
)
|
||||||
|
WHERE fornecedor_id IS NOT NULL AND fornecedor_nome IS NULL;
|
||||||
|
|
||||||
|
-- Remover as constraints das chaves estrangeiras (se existirem)
|
||||||
|
ALTER TABLE despesas DROP CONSTRAINT IF EXISTS despesas_tipo_despesa_id_fkey;
|
||||||
|
ALTER TABLE despesas DROP CONSTRAINT IF EXISTS despesas_fornecedor_id_fkey;
|
||||||
|
|
||||||
|
-- Tornar os campos antigos opcionais (para compatibilidade)
|
||||||
|
ALTER TABLE despesas ALTER COLUMN tipo_despesa_id DROP NOT NULL;
|
||||||
|
|
||||||
|
-- Criar índice nos novos campos para performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_despesas_tipo_nome ON despesas(tipo_nome);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_despesas_fornecedor_nome ON despesas(fornecedor_nome);
|
||||||
|
|
||||||
|
-- Comentários para documentação
|
||||||
|
COMMENT ON COLUMN despesas.tipo_nome IS 'Nome do tipo de despesa (campo livre)';
|
||||||
|
COMMENT ON COLUMN despesas.fornecedor_nome IS 'Nome do fornecedor (campo livre)';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user