Primeiro commit

This commit is contained in:
2025-10-14 14:04:17 -03:00
commit 33d8645eb4
109 changed files with 55424 additions and 0 deletions

79
.env.example Normal file
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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!**

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View 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
View 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
View 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**

View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

45
client/package.json Normal file
View 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

File diff suppressed because it is too large Load Diff

51
client/src/App.js Normal file
View 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;

View 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);
}

View 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;

View 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;
}
}

View 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;

View 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;
}
}

View 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;

View 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
View 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
View 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>
);

View 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;

File diff suppressed because it is too large Load Diff

View 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;

View 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;

View 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;

View 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;

View 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;

View 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 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;

View 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;

View 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

File diff suppressed because it is too large Load Diff

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
View 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;

View 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;
}
}

View 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;
}
}

File diff suppressed because it is too large Load Diff

View 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%;
}
}

View 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;
}
}

View 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
View 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
View 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();

View 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"
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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;

View 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;

View 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

File diff suppressed because it is too large Load Diff

635
server.js Normal file
View 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
View 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** 👶✨

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

129
site/index.html Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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';

View 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);

View 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
View 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';

View 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;

View 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