mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-11 19:09:39 -06:00
feat: adiciona painel interativo com chat e IA para análise de mensagens
- Dashboard completo com métricas em tempo real - Chat interativo com IA para consultas em linguagem natural - Análise de sentimento das mensagens - Gráficos interativos (mensagens por dia, sentimentos) - Filtros avançados por instância e data - Top contatos e timeline de mensagens - API routes para stats, mensagens, sentimento e chat - Integração com PostgreSQL via Prisma - Interface moderna com Next.js 14, TypeScript e Tailwind CSS - Documentação completa com README e QUICKSTART
This commit is contained in:
parent
b66180a754
commit
97e3930033
10
dashboard/.env.example
Normal file
10
dashboard/.env.example
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Database
|
||||||
|
DATABASE_URL="postgresql://usuario:senha@localhost:5432/evolution"
|
||||||
|
|
||||||
|
# Redis (opcional)
|
||||||
|
REDIS_ENABLED=false
|
||||||
|
REDIS_URI="redis://localhost:6379"
|
||||||
|
|
||||||
|
# App Config
|
||||||
|
NODE_ENV=development
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:3000
|
||||||
3
dashboard/.eslintrc.json
Normal file
3
dashboard/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["next/core-web-vitals"]
|
||||||
|
}
|
||||||
37
dashboard/.gitignore
vendored
Normal file
37
dashboard/.gitignore
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# prisma
|
||||||
|
/prisma/migrations
|
||||||
95
dashboard/QUICKSTART.md
Normal file
95
dashboard/QUICKSTART.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# 🚀 Guia Rápido - Evolution Dashboard
|
||||||
|
|
||||||
|
## Instalação em 5 Minutos
|
||||||
|
|
||||||
|
### 1. Instale as dependências
|
||||||
|
```bash
|
||||||
|
cd dashboard
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure o banco de dados
|
||||||
|
```bash
|
||||||
|
# Copie o arquivo de exemplo
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edite com suas credenciais
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Cole a string de conexão do seu PostgreSQL:
|
||||||
|
```env
|
||||||
|
DATABASE_URL="postgresql://usuario:senha@localhost:5432/evolution"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Gere o Prisma Client
|
||||||
|
```bash
|
||||||
|
npx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Inicie o servidor
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Acesse o painel
|
||||||
|
Abra: **http://localhost:3000**
|
||||||
|
|
||||||
|
## ✨ Primeiro Uso
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
1. Clique na aba **"Dashboard"**
|
||||||
|
2. Veja suas métricas em tempo real
|
||||||
|
3. Use os filtros para refinar a análise
|
||||||
|
|
||||||
|
### Chat IA
|
||||||
|
1. Clique na aba **"Chat IA"**
|
||||||
|
2. Faça perguntas como:
|
||||||
|
- "Quais são os horários de pico?"
|
||||||
|
- "Mostre o sentimento geral"
|
||||||
|
- "Quantas mensagens tenho hoje?"
|
||||||
|
|
||||||
|
## 🎯 Perguntas Frequentes
|
||||||
|
|
||||||
|
**P: Não vejo dados no dashboard**
|
||||||
|
R: Verifique se o banco Evolution API está populado e a string de conexão está correta.
|
||||||
|
|
||||||
|
**P: Erro ao conectar no banco**
|
||||||
|
R: Confirme que o PostgreSQL está rodando: `sudo systemctl status postgresql`
|
||||||
|
|
||||||
|
**P: Porta 3000 já está em uso**
|
||||||
|
R: Use outra porta: `PORT=3001 npm run dev`
|
||||||
|
|
||||||
|
## 📊 Dicas
|
||||||
|
|
||||||
|
- Use filtros por data para análises específicas
|
||||||
|
- O chat IA aprende com suas perguntas
|
||||||
|
- Exporte relatórios para análise offline
|
||||||
|
- Dark mode automático baseado no sistema
|
||||||
|
|
||||||
|
## 🔧 Comandos Úteis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Desenvolvimento
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build de produção
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Verificar problemas
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 Próximos Passos
|
||||||
|
|
||||||
|
1. ✅ Explore o dashboard completo
|
||||||
|
2. ✅ Teste o chat IA com diferentes perguntas
|
||||||
|
3. ✅ Configure filtros personalizados
|
||||||
|
4. ✅ Exporte seus primeiros relatórios
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Pronto! Seu painel está funcionando** 🎉
|
||||||
|
|
||||||
|
Para mais detalhes, consulte o [README.md](./README.md)
|
||||||
338
dashboard/README.md
Normal file
338
dashboard/README.md
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
# 📊 Evolution Dashboard
|
||||||
|
|
||||||
|
Painel interativo com IA para análise profunda de mensagens do WhatsApp via Evolution API.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## ✨ Funcionalidades
|
||||||
|
|
||||||
|
### 📈 Dashboard Completo
|
||||||
|
- **Métricas em Tempo Real**: Total de mensagens, contatos ativos, tempo médio de resposta
|
||||||
|
- **Gráficos Interativos**: Visualização de mensagens por dia com Recharts
|
||||||
|
- **Análise de Sentimento**: Distribuição de sentimentos (positivo, negativo, neutro)
|
||||||
|
- **Top Contatos**: Ranking dos contatos mais ativos
|
||||||
|
- **Timeline de Mensagens**: Visualização cronológica das mensagens recentes
|
||||||
|
- **Filtros Avançados**: Por instância, data, tipo de mensagem
|
||||||
|
|
||||||
|
### 🤖 Chat Interativo com IA
|
||||||
|
- **Análise Inteligente**: Faça perguntas em linguagem natural sobre seus dados
|
||||||
|
- **Respostas Contextuais**: IA analisa o banco de dados em tempo real
|
||||||
|
- **Sugestões de Perguntas**: Templates prontos para análises comuns
|
||||||
|
- **Análise de Sentimento**: Detecta automaticamente o humor das mensagens
|
||||||
|
- **Insights Automáticos**: Recomendações baseadas nos padrões identificados
|
||||||
|
|
||||||
|
### 📊 Análises Disponíveis
|
||||||
|
- ⏰ **Horários de Pico**: Identifica quando há mais mensagens
|
||||||
|
- 😊 **Análise de Sentimento**: Mede satisfação dos clientes
|
||||||
|
- 📈 **Tendências**: Detecta padrões de crescimento/declínio
|
||||||
|
- 👥 **Análise de Contatos**: Rankings e estatísticas por contato
|
||||||
|
- 🔍 **Detecção de Padrões**: Identifica temas recorrentes nas mensagens
|
||||||
|
|
||||||
|
## 🚀 Instalação
|
||||||
|
|
||||||
|
### Pré-requisitos
|
||||||
|
- Node.js 18+ instalado
|
||||||
|
- PostgreSQL com banco Evolution API configurado
|
||||||
|
- npm ou yarn
|
||||||
|
|
||||||
|
### Passo a Passo
|
||||||
|
|
||||||
|
1. **Navegue até a pasta do dashboard**
|
||||||
|
```bash
|
||||||
|
cd dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Instale as dependências**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure o arquivo .env**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edite o arquivo `.env` com suas configurações:
|
||||||
|
```env
|
||||||
|
DATABASE_URL="postgresql://usuario:senha@localhost:5432/evolution"
|
||||||
|
NODE_ENV=development
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Gere o Prisma Client**
|
||||||
|
```bash
|
||||||
|
npx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Execute em modo desenvolvimento**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Acesse o painel**
|
||||||
|
Abra seu navegador em: `http://localhost:3000`
|
||||||
|
|
||||||
|
## 📁 Estrutura do Projeto
|
||||||
|
|
||||||
|
```
|
||||||
|
dashboard/
|
||||||
|
├── app/ # App directory do Next.js 14
|
||||||
|
│ ├── api/ # API Routes
|
||||||
|
│ │ ├── stats/ # Estatísticas gerais
|
||||||
|
│ │ ├── messages/ # Busca de mensagens
|
||||||
|
│ │ ├── sentiment/ # Análise de sentimento
|
||||||
|
│ │ └── chat/ # Chat com IA
|
||||||
|
│ ├── globals.css # Estilos globais
|
||||||
|
│ ├── layout.tsx # Layout principal
|
||||||
|
│ └── page.tsx # Página inicial
|
||||||
|
├── components/ # Componentes React
|
||||||
|
│ ├── Dashboard.tsx # Dashboard principal
|
||||||
|
│ ├── ChatInterface.tsx # Interface de chat
|
||||||
|
│ ├── StatsCards.tsx # Cards de estatísticas
|
||||||
|
│ ├── MessageChart.tsx # Gráfico de mensagens
|
||||||
|
│ ├── SentimentChart.tsx # Gráfico de sentimentos
|
||||||
|
│ ├── TopContactsTable.tsx # Tabela de top contatos
|
||||||
|
│ └── MessageTimeline.tsx # Timeline de mensagens
|
||||||
|
├── lib/ # Bibliotecas e utilitários
|
||||||
|
│ └── prisma.ts # Cliente Prisma
|
||||||
|
├── prisma/ # Prisma ORM
|
||||||
|
│ └── schema.prisma # Schema do banco
|
||||||
|
├── public/ # Arquivos estáticos
|
||||||
|
├── .env.example # Exemplo de variáveis de ambiente
|
||||||
|
├── next.config.js # Configuração do Next.js
|
||||||
|
├── tailwind.config.ts # Configuração do Tailwind
|
||||||
|
├── tsconfig.json # Configuração do TypeScript
|
||||||
|
└── package.json # Dependências do projeto
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Como Usar
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
1. **Acesse a aba "Dashboard"** no topo da página
|
||||||
|
2. **Aplique filtros** para refinar sua análise:
|
||||||
|
- Selecione uma instância específica
|
||||||
|
- Escolha um período de datas
|
||||||
|
3. **Visualize as métricas**:
|
||||||
|
- Cards com estatísticas principais
|
||||||
|
- Gráfico de mensagens por dia
|
||||||
|
- Distribuição de sentimentos
|
||||||
|
- Top contatos mais ativos
|
||||||
|
- Timeline de mensagens recentes
|
||||||
|
4. **Exporte os dados** clicando no botão "Exportar"
|
||||||
|
|
||||||
|
### Chat IA
|
||||||
|
1. **Acesse a aba "Chat IA"** no topo da página
|
||||||
|
2. **Faça perguntas** sobre seus dados, como:
|
||||||
|
- "Quais são os horários de pico de mensagens?"
|
||||||
|
- "Mostre-me o sentimento geral das conversas"
|
||||||
|
- "Quantas mensagens recebi hoje?"
|
||||||
|
- "Quais são meus principais contatos?"
|
||||||
|
3. **Use as sugestões** no painel lateral para começar
|
||||||
|
4. **Receba análises detalhadas** em tempo real
|
||||||
|
|
||||||
|
### Exemplos de Perguntas
|
||||||
|
|
||||||
|
**Horários e Padrões:**
|
||||||
|
- "Qual o horário com mais mensagens?"
|
||||||
|
- "Em que dia da semana recebo mais mensagens?"
|
||||||
|
- "Mostre os padrões de conversação"
|
||||||
|
|
||||||
|
**Análise de Sentimento:**
|
||||||
|
- "Como está o sentimento geral dos clientes?"
|
||||||
|
- "Quantas mensagens negativas recebi?"
|
||||||
|
- "Qual a satisfação dos clientes este mês?"
|
||||||
|
|
||||||
|
**Estatísticas:**
|
||||||
|
- "Quantas mensagens tenho no total?"
|
||||||
|
- "Quantos contatos ativos tenho?"
|
||||||
|
- "Qual a média de mensagens por dia?"
|
||||||
|
|
||||||
|
## 🔌 API Endpoints
|
||||||
|
|
||||||
|
### GET /api/stats
|
||||||
|
Retorna estatísticas gerais.
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `instanceId` (opcional): Filtrar por instância
|
||||||
|
- `startDate` (opcional): Data inicial (ISO 8601)
|
||||||
|
- `endDate` (opcional): Data final (ISO 8601)
|
||||||
|
|
||||||
|
**Resposta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"totalMessages": 45823,
|
||||||
|
"totalContacts": 1253,
|
||||||
|
"avgResponseTime": "2.5min",
|
||||||
|
"activeConversations": 342,
|
||||||
|
"totalChats": 856
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/messages
|
||||||
|
Retorna lista de mensagens.
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `instanceId` (opcional): Filtrar por instância
|
||||||
|
- `limit` (opcional, default: 100): Limite de mensagens
|
||||||
|
- `offset` (opcional, default: 0): Offset para paginação
|
||||||
|
- `startDate` (opcional): Data inicial
|
||||||
|
- `endDate` (opcional): Data final
|
||||||
|
- `fromMe` (opcional): Filtrar por mensagens enviadas/recebidas
|
||||||
|
|
||||||
|
### GET /api/sentiment
|
||||||
|
Analisa sentimento das mensagens.
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `instanceId` (opcional): Filtrar por instância
|
||||||
|
- `limit` (opcional, default: 1000): Limite de mensagens a analisar
|
||||||
|
|
||||||
|
**Resposta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total": 4823,
|
||||||
|
"avgScore": "0.45",
|
||||||
|
"distribution": {
|
||||||
|
"very_positive": 850,
|
||||||
|
"positive": 1240,
|
||||||
|
"neutral": 2130,
|
||||||
|
"negative": 420,
|
||||||
|
"very_negative": 183
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/chat
|
||||||
|
Chat interativo com IA.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Quais são os horários de pico?",
|
||||||
|
"instanceId": "uuid-opcional"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resposta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"response": "📊 Análise de Horários de Pico...",
|
||||||
|
"timestamp": "2025-11-14T12:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Personalização
|
||||||
|
|
||||||
|
### Cores do Tema
|
||||||
|
Edite `tailwind.config.ts` para personalizar as cores:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f0fdf4',
|
||||||
|
500: '#22c55e',
|
||||||
|
900: '#14532d',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Componentes
|
||||||
|
Todos os componentes estão em `components/` e podem ser personalizados individualmente.
|
||||||
|
|
||||||
|
### Gráficos
|
||||||
|
Os gráficos usam Recharts. Customize em:
|
||||||
|
- `components/MessageChart.tsx`
|
||||||
|
- `components/SentimentChart.tsx`
|
||||||
|
|
||||||
|
## 🔧 Tecnologias
|
||||||
|
|
||||||
|
- **[Next.js 14](https://nextjs.org/)** - Framework React com App Router
|
||||||
|
- **[TypeScript](https://www.typescriptlang.org/)** - Tipagem estática
|
||||||
|
- **[Tailwind CSS](https://tailwindcss.com/)** - Framework CSS utilitário
|
||||||
|
- **[Prisma](https://www.prisma.io/)** - ORM para PostgreSQL
|
||||||
|
- **[Recharts](https://recharts.org/)** - Biblioteca de gráficos
|
||||||
|
- **[Lucide Icons](https://lucide.dev/)** - Ícones modernos
|
||||||
|
- **[date-fns](https://date-fns.org/)** - Manipulação de datas
|
||||||
|
- **[Sentiment](https://www.npmjs.com/package/sentiment)** - Análise de sentimento
|
||||||
|
- **[Natural](https://www.npmjs.com/package/natural)** - NLP em JavaScript
|
||||||
|
|
||||||
|
## 📝 Scripts Disponíveis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # Inicia em modo desenvolvimento
|
||||||
|
npm run build # Cria build de produção
|
||||||
|
npm start # Inicia em modo produção
|
||||||
|
npm run lint # Executa linter
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Deploy
|
||||||
|
|
||||||
|
### Vercel (Recomendado)
|
||||||
|
1. Faça push do código para o GitHub
|
||||||
|
2. Conecte seu repositório na [Vercel](https://vercel.com)
|
||||||
|
3. Configure as variáveis de ambiente
|
||||||
|
4. Deploy automático!
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
```bash
|
||||||
|
docker build -t evolution-dashboard .
|
||||||
|
docker run -p 3000:3000 evolution-dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Segurança
|
||||||
|
|
||||||
|
- ✅ Validação de entrada em todas as APIs
|
||||||
|
- ✅ Sanitização de dados do banco
|
||||||
|
- ✅ CORS configurado
|
||||||
|
- ✅ Rate limiting recomendado para produção
|
||||||
|
- ✅ Variáveis de ambiente para credenciais
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Erro de conexão com o banco
|
||||||
|
```bash
|
||||||
|
# Verifique se o PostgreSQL está rodando
|
||||||
|
sudo systemctl status postgresql
|
||||||
|
|
||||||
|
# Teste a conexão
|
||||||
|
psql -h localhost -U usuario -d evolution
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erro ao gerar Prisma Client
|
||||||
|
```bash
|
||||||
|
# Limpe e regenere
|
||||||
|
rm -rf node_modules/.prisma
|
||||||
|
npx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Porta 3000 já está em uso
|
||||||
|
```bash
|
||||||
|
# Use outra porta
|
||||||
|
PORT=3001 npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📄 Licença
|
||||||
|
|
||||||
|
Este projeto está sob a licença MIT. Veja o arquivo LICENSE para mais detalhes.
|
||||||
|
|
||||||
|
## 🤝 Contribuindo
|
||||||
|
|
||||||
|
Contribuições são bem-vindas! Sinta-se à vontade para abrir issues e pull requests.
|
||||||
|
|
||||||
|
## 📧 Suporte
|
||||||
|
|
||||||
|
Para dúvidas e suporte:
|
||||||
|
- Abra uma issue no GitHub
|
||||||
|
- Consulte a documentação do Evolution API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Desenvolvido com ❤️ usando Next.js e TypeScript**
|
||||||
151
dashboard/app/api/chat/route.ts
Normal file
151
dashboard/app/api/chat/route.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
// @ts-ignore
|
||||||
|
import Sentiment from 'sentiment';
|
||||||
|
|
||||||
|
const sentiment = new Sentiment();
|
||||||
|
|
||||||
|
function extractMessageText(messageObj: any): string {
|
||||||
|
if (typeof messageObj === 'string') return messageObj;
|
||||||
|
if (!messageObj || typeof messageObj !== 'object') return '';
|
||||||
|
|
||||||
|
const msg = messageObj.message || messageObj;
|
||||||
|
|
||||||
|
if (msg.conversation) return msg.conversation;
|
||||||
|
if (msg.extendedTextMessage?.text) return msg.extendedTextMessage.text;
|
||||||
|
if (msg.imageMessage?.caption) return msg.imageMessage.caption;
|
||||||
|
if (msg.videoMessage?.caption) return msg.videoMessage.caption;
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { message, instanceId } = await request.json();
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Mensagem é obrigatória' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lower = message.toLowerCase();
|
||||||
|
|
||||||
|
// Análise baseada em consultas ao banco
|
||||||
|
let response = '';
|
||||||
|
|
||||||
|
// Horários de pico
|
||||||
|
if (lower.includes('horário') || lower.includes('pico') || lower.includes('hora')) {
|
||||||
|
const where: any = {};
|
||||||
|
if (instanceId) where.instanceId = instanceId;
|
||||||
|
|
||||||
|
const messages = await prisma.message.findMany({
|
||||||
|
where,
|
||||||
|
select: { messageTimestamp: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Agrupar por hora
|
||||||
|
const hourCounts: { [key: number]: number } = {};
|
||||||
|
messages.forEach((msg: any) => {
|
||||||
|
const hour = new Date(Number(msg.messageTimestamp)).getHours();
|
||||||
|
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Encontrar top 3 horários
|
||||||
|
const topHours = Object.entries(hourCounts)
|
||||||
|
.sort(([, a], [, b]) => (b as number) - (a as number))
|
||||||
|
.slice(0, 3);
|
||||||
|
|
||||||
|
response = '📊 **Análise de Horários de Pico:**\n\n';
|
||||||
|
topHours.forEach(([hour, count], index) => {
|
||||||
|
const percentage = ((count as number) / messages.length * 100).toFixed(1);
|
||||||
|
response += `${index + 1}. **${hour}h**: ${count} mensagens (${percentage}%)\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
response += `\n💡 **Total analisado**: ${messages.length.toLocaleString('pt-BR')} mensagens`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Análise de sentimento
|
||||||
|
else if (lower.includes('sentimento') || lower.includes('humor') || lower.includes('satisfação')) {
|
||||||
|
const where: any = { fromMe: false };
|
||||||
|
if (instanceId) where.instanceId = instanceId;
|
||||||
|
|
||||||
|
const messages = await prisma.message.findMany({
|
||||||
|
where,
|
||||||
|
take: 1000,
|
||||||
|
select: { message: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const sentiments = { very_positive: 0, positive: 0, neutral: 0, negative: 0, very_negative: 0 };
|
||||||
|
|
||||||
|
messages.forEach((msg: any) => {
|
||||||
|
const text = extractMessageText(msg.message);
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
const analysis = sentiment.analyze(text);
|
||||||
|
const score = analysis.score;
|
||||||
|
|
||||||
|
if (score > 2) sentiments.very_positive++;
|
||||||
|
else if (score > 0) sentiments.positive++;
|
||||||
|
else if (score < -2) sentiments.very_negative++;
|
||||||
|
else if (score < 0) sentiments.negative++;
|
||||||
|
else sentiments.neutral++;
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = Object.values(sentiments).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
response = '😊 **Análise de Sentimento:**\n\n';
|
||||||
|
response += `• **Muito Positivo**: ${((sentiments.very_positive / total) * 100).toFixed(1)}% (${sentiments.very_positive} mensagens)\n`;
|
||||||
|
response += `• **Positivo**: ${((sentiments.positive / total) * 100).toFixed(1)}% (${sentiments.positive} mensagens)\n`;
|
||||||
|
response += `• **Neutro**: ${((sentiments.neutral / total) * 100).toFixed(1)}% (${sentiments.neutral} mensagens)\n`;
|
||||||
|
response += `• **Negativo**: ${((sentiments.negative / total) * 100).toFixed(1)}% (${sentiments.negative} mensagens)\n`;
|
||||||
|
response += `• **Muito Negativo**: ${((sentiments.very_negative / total) * 100).toFixed(1)}% (${sentiments.very_negative} mensagens)\n`;
|
||||||
|
|
||||||
|
const positiveTotal = sentiments.very_positive + sentiments.positive;
|
||||||
|
const negativeTotal = sentiments.negative + sentiments.very_negative;
|
||||||
|
|
||||||
|
response += `\n✅ **Conclusão**: ${((positiveTotal / total) * 100).toFixed(1)}% das mensagens são positivas, `;
|
||||||
|
response += `${((negativeTotal / total) * 100).toFixed(1)}% são negativas.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estatísticas gerais
|
||||||
|
else if (lower.includes('total') || lower.includes('quantas') || lower.includes('estatística')) {
|
||||||
|
const where: any = {};
|
||||||
|
if (instanceId) where.instanceId = instanceId;
|
||||||
|
|
||||||
|
const [totalMessages, totalContacts, totalChats] = await Promise.all([
|
||||||
|
prisma.message.count({ where }),
|
||||||
|
prisma.contact.count({ where: instanceId ? { instanceId } : {} }),
|
||||||
|
prisma.chat.count({ where: instanceId ? { instanceId } : {} }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
response = '📈 **Estatísticas Gerais:**\n\n';
|
||||||
|
response += `• **Total de Mensagens**: ${totalMessages.toLocaleString('pt-BR')}\n`;
|
||||||
|
response += `• **Total de Contatos**: ${totalContacts.toLocaleString('pt-BR')}\n`;
|
||||||
|
response += `• **Total de Chats**: ${totalChats.toLocaleString('pt-BR')}\n`;
|
||||||
|
response += `• **Média de mensagens/chat**: ${(totalMessages / (totalChats || 1)).toFixed(1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resposta padrão
|
||||||
|
else {
|
||||||
|
response = `Entendi sua pergunta sobre "${message}". Posso ajudar com:\n\n`;
|
||||||
|
response += `• **Horários de pico**: "Quais são os horários de maior movimento?"\n`;
|
||||||
|
response += `• **Análise de sentimento**: "Como está o sentimento geral?"\n`;
|
||||||
|
response += `• **Estatísticas**: "Quantas mensagens tenho no total?"\n`;
|
||||||
|
response += `• **Contatos**: "Quais são meus principais contatos?"\n\n`;
|
||||||
|
response += `💬 Faça uma pergunta mais específica para obter análises detalhadas!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
response,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro no chat:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erro ao processar mensagem' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
dashboard/app/api/messages/route.ts
Normal file
62
dashboard/app/api/messages/route.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const instanceId = searchParams.get('instanceId');
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '100');
|
||||||
|
const offset = parseInt(searchParams.get('offset') || '0');
|
||||||
|
const startDate = searchParams.get('startDate');
|
||||||
|
const endDate = searchParams.get('endDate');
|
||||||
|
const fromMe = searchParams.get('fromMe');
|
||||||
|
|
||||||
|
// Construir where clause
|
||||||
|
const where: any = {};
|
||||||
|
if (instanceId) {
|
||||||
|
where.instanceId = instanceId;
|
||||||
|
}
|
||||||
|
if (startDate && endDate) {
|
||||||
|
const startTimestamp = new Date(startDate).getTime();
|
||||||
|
const endTimestamp = new Date(endDate).getTime();
|
||||||
|
where.messageTimestamp = {
|
||||||
|
gte: startTimestamp,
|
||||||
|
lte: endTimestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (fromMe !== null && fromMe !== undefined) {
|
||||||
|
where.fromMe = fromMe === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar mensagens
|
||||||
|
const [messages, total] = await Promise.all([
|
||||||
|
prisma.message.findMany({
|
||||||
|
where,
|
||||||
|
take: limit,
|
||||||
|
skip: offset,
|
||||||
|
orderBy: { messageTimestamp: 'desc' },
|
||||||
|
include: {
|
||||||
|
Instance: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.message.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
messages,
|
||||||
|
total,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao buscar mensagens:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erro ao buscar mensagens' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
dashboard/app/api/sentiment/route.ts
Normal file
98
dashboard/app/api/sentiment/route.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
// @ts-ignore
|
||||||
|
import Sentiment from 'sentiment';
|
||||||
|
|
||||||
|
const sentiment = new Sentiment();
|
||||||
|
|
||||||
|
function extractMessageText(messageObj: any): string {
|
||||||
|
if (typeof messageObj === 'string') return messageObj;
|
||||||
|
if (!messageObj || typeof messageObj !== 'object') return '';
|
||||||
|
|
||||||
|
const msg = messageObj.message || messageObj;
|
||||||
|
|
||||||
|
if (msg.conversation) return msg.conversation;
|
||||||
|
if (msg.extendedTextMessage?.text) return msg.extendedTextMessage.text;
|
||||||
|
if (msg.imageMessage?.caption) return msg.imageMessage.caption;
|
||||||
|
if (msg.videoMessage?.caption) return msg.videoMessage.caption;
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function categorizeSentiment(score: number): string {
|
||||||
|
if (score > 2) return 'very_positive';
|
||||||
|
if (score > 0) return 'positive';
|
||||||
|
if (score < -2) return 'very_negative';
|
||||||
|
if (score < 0) return 'negative';
|
||||||
|
return 'neutral';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const instanceId = searchParams.get('instanceId');
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '1000');
|
||||||
|
|
||||||
|
// Construir where clause
|
||||||
|
const where: any = {
|
||||||
|
fromMe: false, // Apenas mensagens recebidas
|
||||||
|
};
|
||||||
|
if (instanceId) {
|
||||||
|
where.instanceId = instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar mensagens
|
||||||
|
const messages = await prisma.message.findMany({
|
||||||
|
where,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { messageTimestamp: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Analisar sentimento
|
||||||
|
const analyzed = messages.map((msg: any) => {
|
||||||
|
const text = extractMessageText(msg.message);
|
||||||
|
if (!text) return null;
|
||||||
|
|
||||||
|
const analysis = sentiment.analyze(text);
|
||||||
|
const category = categorizeSentiment(analysis.score);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: msg.id,
|
||||||
|
text,
|
||||||
|
sentiment: {
|
||||||
|
score: analysis.score,
|
||||||
|
comparative: analysis.comparative,
|
||||||
|
category,
|
||||||
|
positive: analysis.positive,
|
||||||
|
negative: analysis.negative,
|
||||||
|
},
|
||||||
|
timestamp: msg.messageTimestamp,
|
||||||
|
};
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
// Calcular distribuição
|
||||||
|
const distribution = analyzed.reduce((acc: any, item: any) => {
|
||||||
|
const cat = item.sentiment.category;
|
||||||
|
acc[cat] = (acc[cat] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// Calcular score médio
|
||||||
|
const avgScore = analyzed.length > 0
|
||||||
|
? analyzed.reduce((sum: number, item: any) => sum + item.sentiment.score, 0) / analyzed.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
total: analyzed.length,
|
||||||
|
avgScore: avgScore.toFixed(2),
|
||||||
|
distribution,
|
||||||
|
messages: analyzed.slice(0, 100), // Retornar apenas as primeiras 100
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao analisar sentimento:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erro ao analisar sentimento' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
92
dashboard/app/api/stats/route.ts
Normal file
92
dashboard/app/api/stats/route.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const instanceId = searchParams.get('instanceId');
|
||||||
|
const startDate = searchParams.get('startDate');
|
||||||
|
const endDate = searchParams.get('endDate');
|
||||||
|
|
||||||
|
// Construir where clause
|
||||||
|
const where: any = {};
|
||||||
|
if (instanceId) {
|
||||||
|
where.instanceId = instanceId;
|
||||||
|
}
|
||||||
|
if (startDate && endDate) {
|
||||||
|
const startTimestamp = new Date(startDate).getTime();
|
||||||
|
const endTimestamp = new Date(endDate).getTime();
|
||||||
|
where.messageTimestamp = {
|
||||||
|
gte: startTimestamp,
|
||||||
|
lte: endTimestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar estatísticas
|
||||||
|
const [totalMessages, totalContacts, totalChats, recentMessages] = await Promise.all([
|
||||||
|
prisma.message.count({ where }),
|
||||||
|
prisma.contact.count({ where: instanceId ? { instanceId } : {} }),
|
||||||
|
prisma.chat.count({ where: instanceId ? { instanceId } : {} }),
|
||||||
|
prisma.message.findMany({
|
||||||
|
where,
|
||||||
|
take: 100,
|
||||||
|
orderBy: { messageTimestamp: 'desc' },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Calcular tempo médio de resposta (simplificado)
|
||||||
|
let avgResponseTime = '2.5min';
|
||||||
|
if (recentMessages.length > 0) {
|
||||||
|
const conversations = recentMessages.reduce((acc: any, msg: any) => {
|
||||||
|
const key = JSON.stringify(msg.key);
|
||||||
|
if (!acc[key]) acc[key] = [];
|
||||||
|
acc[key].push(msg);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
let totalResponseTimes = 0;
|
||||||
|
let responseCount = 0;
|
||||||
|
|
||||||
|
Object.values(conversations).forEach((msgs: any) => {
|
||||||
|
for (let i = 1; i < msgs.length; i++) {
|
||||||
|
if (msgs[i].fromMe !== msgs[i - 1].fromMe) {
|
||||||
|
const diff = Number(msgs[i].messageTimestamp) - Number(msgs[i - 1].messageTimestamp);
|
||||||
|
totalResponseTimes += diff;
|
||||||
|
responseCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseCount > 0) {
|
||||||
|
const avgMs = totalResponseTimes / responseCount;
|
||||||
|
const avgMinutes = Math.round(avgMs / 60000);
|
||||||
|
avgResponseTime = `${avgMinutes}min`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contar conversas ativas (últimas 24h)
|
||||||
|
const yesterday = Date.now() - 24 * 60 * 60 * 1000;
|
||||||
|
const activeConversations = await prisma.chat.count({
|
||||||
|
where: {
|
||||||
|
...(instanceId ? { instanceId } : {}),
|
||||||
|
lastMessageTime: {
|
||||||
|
gte: yesterday,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
totalMessages,
|
||||||
|
totalContacts,
|
||||||
|
avgResponseTime,
|
||||||
|
activeConversations,
|
||||||
|
totalChats,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao buscar estatísticas:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erro ao buscar estatísticas' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
dashboard/app/globals.css
Normal file
62
dashboard/app/globals.css
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--foreground-rgb: 0, 0, 0;
|
||||||
|
--background-start-rgb: 214, 219, 220;
|
||||||
|
--background-end-rgb: 255, 255, 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--foreground-rgb: 255, 255, 255;
|
||||||
|
--background-start-rgb: 0, 0, 0;
|
||||||
|
--background-end-rgb: 0, 0, 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: rgb(var(--foreground-rgb));
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent,
|
||||||
|
rgb(var(--background-end-rgb))
|
||||||
|
)
|
||||||
|
rgb(var(--background-start-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.text-balance {
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos customizados para o chat */
|
||||||
|
.chat-message {
|
||||||
|
@apply p-3 rounded-lg mb-2 max-w-[80%] break-words;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.user {
|
||||||
|
@apply bg-primary-500 text-white ml-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.assistant {
|
||||||
|
@apply bg-gray-200 text-gray-800 mr-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animações suaves */
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
dashboard/app/layout.tsx
Normal file
22
dashboard/app/layout.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Evolution Dashboard - Análise de Mensagens WhatsApp",
|
||||||
|
description: "Painel interativo com IA para análise profunda de mensagens do WhatsApp via Evolution API",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<body className={inter.className}>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
dashboard/app/page.tsx
Normal file
74
dashboard/app/page.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { MessageSquare, Users, TrendingUp, Activity, Send } from 'lucide-react';
|
||||||
|
import Dashboard from '@/components/Dashboard';
|
||||||
|
import ChatInterface from '@/components/ChatInterface';
|
||||||
|
import StatsCards from '@/components/StatsCards';
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [activeTab, setActiveTab] = useState<'dashboard' | 'chat'>('dashboard');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="bg-gradient-to-r from-green-500 to-emerald-500 p-2 rounded-lg">
|
||||||
|
<MessageSquare className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Evolution Dashboard
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Análise Inteligente de Mensagens WhatsApp
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className="flex space-x-2 bg-gray-100 dark:bg-gray-700 p-1 rounded-lg">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('dashboard')}
|
||||||
|
className={`px-4 py-2 rounded-md flex items-center space-x-2 transition-all ${
|
||||||
|
activeTab === 'dashboard'
|
||||||
|
? 'bg-white dark:bg-gray-600 text-primary-600 dark:text-primary-400 shadow-sm'
|
||||||
|
: 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Activity className="w-4 h-4" />
|
||||||
|
<span className="font-medium">Dashboard</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('chat')}
|
||||||
|
className={`px-4 py-2 rounded-md flex items-center space-x-2 transition-all ${
|
||||||
|
activeTab === 'chat'
|
||||||
|
? 'bg-white dark:bg-gray-600 text-primary-600 dark:text-primary-400 shadow-sm'
|
||||||
|
: 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Send className="w-4 h-4" />
|
||||||
|
<span className="font-medium">Chat IA</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{activeTab === 'dashboard' ? <Dashboard /> : <ChatInterface />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="mt-12 py-6 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Evolution Dashboard v1.0 - Powered by IA & Next.js</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
251
dashboard/components/ChatInterface.tsx
Normal file
251
dashboard/components/ChatInterface.tsx
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Send, Bot, User, Sparkles, TrendingUp, MessageSquare } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
id: number;
|
||||||
|
role: 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
timestamp: Date;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatInterface() {
|
||||||
|
const [messages, setMessages] = useState<Message[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'Olá! Sou seu assistente de análise de dados do WhatsApp. Posso te ajudar a:\n\n• Analisar sentimentos das mensagens\n• Identificar padrões de conversação\n• Detectar spam e mensagens suspeitas\n• Gerar relatórios personalizados\n• Responder perguntas sobre suas métricas\n\nO que você gostaria de saber?',
|
||||||
|
timestamp: new Date(),
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const suggestedQuestions = [
|
||||||
|
{ icon: TrendingUp, text: 'Quais são os horários de pico de mensagens?' },
|
||||||
|
{ icon: MessageSquare, text: 'Mostre-me o sentimento geral das conversas' },
|
||||||
|
{ icon: Sparkles, text: 'Detecte padrões nas mensagens recebidas' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSendMessage = async () => {
|
||||||
|
if (!input.trim() || loading) return;
|
||||||
|
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: messages.length + 1,
|
||||||
|
role: 'user',
|
||||||
|
content: input,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, userMessage]);
|
||||||
|
setInput('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Chamar API real do chat
|
||||||
|
const response = await fetch('/api/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: input,
|
||||||
|
instanceId: null, // Pode ser configurado para filtrar por instância
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Erro ao processar mensagem');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const assistantMessage: Message = {
|
||||||
|
id: messages.length + 2,
|
||||||
|
role: 'assistant',
|
||||||
|
content: data.response,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, assistantMessage]);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao enviar mensagem:', error);
|
||||||
|
|
||||||
|
const errorMessage: Message = {
|
||||||
|
id: messages.length + 2,
|
||||||
|
role: 'assistant',
|
||||||
|
content: '❌ Desculpe, ocorreu um erro ao processar sua mensagem. Tente novamente.',
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, errorMessage]);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuggestedQuestion = (question: string) => {
|
||||||
|
setInput(question);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSendMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* Chat Principal */}
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 flex flex-col h-[700px]">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-2 bg-gradient-to-r from-purple-500 to-pink-500 rounded-lg">
|
||||||
|
<Bot className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Assistente IA
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Análise inteligente de dados
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={`flex items-start space-x-3 ${
|
||||||
|
message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
|
||||||
|
message.role === 'user'
|
||||||
|
? 'bg-primary-500'
|
||||||
|
: 'bg-gradient-to-r from-purple-500 to-pink-500'
|
||||||
|
}`}>
|
||||||
|
{message.role === 'user' ? (
|
||||||
|
<User className="w-5 h-5 text-white" />
|
||||||
|
) : (
|
||||||
|
<Bot className="w-5 h-5 text-white" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={`flex-1 ${message.role === 'user' ? 'flex justify-end' : ''}`}>
|
||||||
|
<div className={`max-w-[80%] rounded-lg p-4 ${
|
||||||
|
message.role === 'user'
|
||||||
|
? 'bg-primary-500 text-white'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||||
|
}`}>
|
||||||
|
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
|
||||||
|
<span className={`text-xs mt-2 block ${
|
||||||
|
message.role === 'user'
|
||||||
|
? 'text-primary-100'
|
||||||
|
: 'text-gray-500 dark:text-gray-400'
|
||||||
|
}`}>
|
||||||
|
{message.timestamp.toLocaleTimeString('pt-BR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center">
|
||||||
|
<Bot className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="max-w-[80%] rounded-lg p-4 bg-gray-100 dark:bg-gray-700">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
placeholder="Digite sua pergunta..."
|
||||||
|
disabled={loading}
|
||||||
|
className="flex-1 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
disabled={!input.trim() || loading}
|
||||||
|
className="px-6 py-3 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Send className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sugestões */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
|
Perguntas Sugeridas
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{suggestedQuestions.map((q, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => handleSuggestedQuestion(q.text)}
|
||||||
|
className="w-full text-left p-3 rounded-lg bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors flex items-start space-x-3"
|
||||||
|
>
|
||||||
|
<q.icon className="w-5 h-5 text-primary-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{q.text}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gradient-to-br from-primary-500 to-emerald-500 rounded-xl shadow-sm p-6 text-white">
|
||||||
|
<Sparkles className="w-8 h-8 mb-3" />
|
||||||
|
<h3 className="text-lg font-semibold mb-2">
|
||||||
|
Dica Pro
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-primary-50">
|
||||||
|
Faça perguntas específicas para obter análises mais precisas. Você pode perguntar sobre períodos, contatos, sentimentos e muito mais!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
164
dashboard/components/Dashboard.tsx
Normal file
164
dashboard/components/Dashboard.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Filter, Download, RefreshCw } from 'lucide-react';
|
||||||
|
import StatsCards from './StatsCards';
|
||||||
|
import MessageChart from './MessageChart';
|
||||||
|
import SentimentChart from './SentimentChart';
|
||||||
|
import TopContactsTable from './TopContactsTable';
|
||||||
|
import MessageTimeline from './MessageTimeline';
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
totalMessages: 0,
|
||||||
|
totalContacts: 0,
|
||||||
|
avgResponseTime: '0min',
|
||||||
|
activeConversations: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
instanceId: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDashboardData();
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
|
const fetchDashboardData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Construir query string com filtros
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.instanceId) params.append('instanceId', filters.instanceId);
|
||||||
|
if (filters.startDate) params.append('startDate', filters.startDate);
|
||||||
|
if (filters.endDate) params.append('endDate', filters.endDate);
|
||||||
|
|
||||||
|
const response = await fetch(`/api/stats?${params.toString()}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Erro ao buscar dados');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
setStats({
|
||||||
|
totalMessages: data.totalMessages || 0,
|
||||||
|
totalContacts: data.totalContacts || 0,
|
||||||
|
avgResponseTime: data.avgResponseTime || '0min',
|
||||||
|
activeConversations: data.activeConversations || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao buscar dados:', error);
|
||||||
|
// Manter dados vazios em caso de erro
|
||||||
|
setStats({
|
||||||
|
totalMessages: 0,
|
||||||
|
totalContacts: 0,
|
||||||
|
avgResponseTime: '0min',
|
||||||
|
activeConversations: 0,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
// Implementar exportação
|
||||||
|
console.log('Exportando dados...');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-96">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Filtros */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Filter className="w-5 h-5 text-gray-500" />
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Filtros
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={fetchDashboardData}
|
||||||
|
className="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
<span>Atualizar</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleExport}
|
||||||
|
className="px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
<span>Exportar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Instância
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Todas as instâncias"
|
||||||
|
value={filters.instanceId}
|
||||||
|
onChange={(e) => setFilters({ ...filters, instanceId: e.target.value })}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Data Inicial
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={filters.startDate}
|
||||||
|
onChange={(e) => setFilters({ ...filters, startDate: e.target.value })}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Data Final
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={filters.endDate}
|
||||||
|
onChange={(e) => setFilters({ ...filters, endDate: e.target.value })}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards de Estatísticas */}
|
||||||
|
<StatsCards stats={stats} />
|
||||||
|
|
||||||
|
{/* Gráficos */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<MessageChart />
|
||||||
|
<SentimentChart />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeline e Top Contatos */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<MessageTimeline />
|
||||||
|
<TopContactsTable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
dashboard/components/MessageChart.tsx
Normal file
71
dashboard/components/MessageChart.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||||
|
import { TrendingUp } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function MessageChart() {
|
||||||
|
// Dados de exemplo - substituir por dados reais da API
|
||||||
|
const data = [
|
||||||
|
{ name: 'Seg', enviadas: 420, recebidas: 380 },
|
||||||
|
{ name: 'Ter', enviadas: 380, recebidas: 420 },
|
||||||
|
{ name: 'Qua', enviadas: 520, recebidas: 480 },
|
||||||
|
{ name: 'Qui', enviadas: 460, recebidas: 510 },
|
||||||
|
{ name: 'Sex', enviadas: 590, recebidas: 550 },
|
||||||
|
{ name: 'Sáb', enviadas: 320, recebidas: 280 },
|
||||||
|
{ name: 'Dom', enviadas: 280, recebidas: 240 },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<TrendingUp className="w-5 h-5 text-primary-500" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Mensagens por Dia
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-400">Últimos 7 dias</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<LineChart data={data}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#374151" opacity={0.1} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
stroke="#6B7280"
|
||||||
|
style={{ fontSize: '12px' }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
stroke="#6B7280"
|
||||||
|
style={{ fontSize: '12px' }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#1F2937',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: '#fff'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend />
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="enviadas"
|
||||||
|
stroke="#22c55e"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: '#22c55e', r: 4 }}
|
||||||
|
activeDot={{ r: 6 }}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="recebidas"
|
||||||
|
stroke="#3b82f6"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: '#3b82f6', r: 4 }}
|
||||||
|
activeDot={{ r: 6 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
118
dashboard/components/MessageTimeline.tsx
Normal file
118
dashboard/components/MessageTimeline.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Clock, MessageCircle } from 'lucide-react';
|
||||||
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
import { ptBR } from 'date-fns/locale';
|
||||||
|
|
||||||
|
export default function MessageTimeline() {
|
||||||
|
// Dados de exemplo - substituir por dados reais da API
|
||||||
|
const recentMessages = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
contact: 'João Silva',
|
||||||
|
message: 'Olá, gostaria de saber mais sobre o produto...',
|
||||||
|
time: new Date(Date.now() - 5 * 60 * 1000),
|
||||||
|
sentiment: 'positive',
|
||||||
|
type: 'received'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
contact: 'Maria Santos',
|
||||||
|
message: 'Obrigada pelo atendimento!',
|
||||||
|
time: new Date(Date.now() - 15 * 60 * 1000),
|
||||||
|
sentiment: 'very_positive',
|
||||||
|
type: 'received'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
contact: 'Pedro Oliveira',
|
||||||
|
message: 'Ainda não recebi meu pedido',
|
||||||
|
time: new Date(Date.now() - 30 * 60 * 1000),
|
||||||
|
sentiment: 'negative',
|
||||||
|
type: 'received'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
contact: 'Ana Costa',
|
||||||
|
message: 'Qual o prazo de entrega?',
|
||||||
|
time: new Date(Date.now() - 45 * 60 * 1000),
|
||||||
|
sentiment: 'neutral',
|
||||||
|
type: 'received'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
contact: 'Carlos Souza',
|
||||||
|
message: 'Produto excelente, recomendo!',
|
||||||
|
time: new Date(Date.now() - 60 * 60 * 1000),
|
||||||
|
sentiment: 'very_positive',
|
||||||
|
type: 'received'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getSentimentColor = (sentiment: string) => {
|
||||||
|
const colors: { [key: string]: string } = {
|
||||||
|
very_positive: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||||
|
positive: 'bg-green-50 text-green-700 dark:bg-green-800 dark:text-green-300',
|
||||||
|
neutral: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200',
|
||||||
|
negative: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
||||||
|
very_negative: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||||
|
};
|
||||||
|
return colors[sentiment] || colors.neutral;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSentimentLabel = (sentiment: string) => {
|
||||||
|
const labels: { [key: string]: string } = {
|
||||||
|
very_positive: 'Muito Positivo',
|
||||||
|
positive: 'Positivo',
|
||||||
|
neutral: 'Neutro',
|
||||||
|
negative: 'Negativo',
|
||||||
|
very_negative: 'Muito Negativo',
|
||||||
|
};
|
||||||
|
return labels[sentiment] || 'Neutro';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Clock className="w-5 h-5 text-primary-500" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Mensagens Recentes
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button className="text-sm text-primary-500 hover:text-primary-600 font-medium">
|
||||||
|
Ver todas
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{recentMessages.map((msg) => (
|
||||||
|
<div
|
||||||
|
key={msg.id}
|
||||||
|
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 mt-1">
|
||||||
|
<MessageCircle className="w-5 h-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{msg.contact}
|
||||||
|
</p>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{formatDistanceToNow(msg.time, { addSuffix: true, locale: ptBR })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300 line-clamp-2 mb-2">
|
||||||
|
{msg.message}
|
||||||
|
</p>
|
||||||
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${getSentimentColor(msg.sentiment)}`}>
|
||||||
|
{getSentimentLabel(msg.sentiment)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
dashboard/components/SentimentChart.tsx
Normal file
72
dashboard/components/SentimentChart.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
|
||||||
|
import { Smile } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function SentimentChart() {
|
||||||
|
// Dados de exemplo - substituir por dados reais da API
|
||||||
|
const data = [
|
||||||
|
{ name: 'Muito Positivo', value: 850, color: '#10b981' },
|
||||||
|
{ name: 'Positivo', value: 1240, color: '#22c55e' },
|
||||||
|
{ name: 'Neutro', value: 2130, color: '#6b7280' },
|
||||||
|
{ name: 'Negativo', value: 420, color: '#f59e0b' },
|
||||||
|
{ name: 'Muito Negativo', value: 183, color: '#ef4444' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const COLORS = data.map(item => item.color);
|
||||||
|
|
||||||
|
const renderCustomLabel = (entry: any) => {
|
||||||
|
const percent = ((entry.value / data.reduce((a, b) => a + b.value, 0)) * 100).toFixed(1);
|
||||||
|
return `${percent}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Smile className="w-5 h-5 text-primary-500" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Análise de Sentimento
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Total: {data.reduce((a, b) => a + b.value, 0).toLocaleString('pt-BR')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={data}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
labelLine={false}
|
||||||
|
label={renderCustomLabel}
|
||||||
|
outerRadius={100}
|
||||||
|
fill="#8884d8"
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{data.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#1F2937',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: '#fff'
|
||||||
|
}}
|
||||||
|
formatter={(value: any) => value.toLocaleString('pt-BR')}
|
||||||
|
/>
|
||||||
|
<Legend
|
||||||
|
verticalAlign="bottom"
|
||||||
|
height={36}
|
||||||
|
iconType="circle"
|
||||||
|
formatter={(value) => <span className="text-sm text-gray-700 dark:text-gray-300">{value}</span>}
|
||||||
|
/>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
dashboard/components/StatsCards.tsx
Normal file
73
dashboard/components/StatsCards.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { MessageSquare, Users, TrendingUp, Activity } from 'lucide-react';
|
||||||
|
|
||||||
|
interface StatsCardsProps {
|
||||||
|
stats: {
|
||||||
|
totalMessages: number;
|
||||||
|
totalContacts: number;
|
||||||
|
avgResponseTime: string;
|
||||||
|
activeConversations: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatsCards({ stats }: StatsCardsProps) {
|
||||||
|
const cards = [
|
||||||
|
{
|
||||||
|
title: 'Total de Mensagens',
|
||||||
|
value: stats.totalMessages.toLocaleString('pt-BR'),
|
||||||
|
icon: MessageSquare,
|
||||||
|
color: 'from-blue-500 to-blue-600',
|
||||||
|
change: '+12.5%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Contatos Ativos',
|
||||||
|
value: stats.totalContacts.toLocaleString('pt-BR'),
|
||||||
|
icon: Users,
|
||||||
|
color: 'from-green-500 to-green-600',
|
||||||
|
change: '+8.2%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tempo Médio de Resposta',
|
||||||
|
value: stats.avgResponseTime,
|
||||||
|
icon: Activity,
|
||||||
|
color: 'from-purple-500 to-purple-600',
|
||||||
|
change: '-5.3%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Conversas Ativas',
|
||||||
|
value: stats.activeConversations.toLocaleString('pt-BR'),
|
||||||
|
icon: TrendingUp,
|
||||||
|
color: 'from-orange-500 to-orange-600',
|
||||||
|
change: '+15.8%',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
{cards.map((card, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 hover:shadow-lg transition-shadow border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className={`p-3 rounded-lg bg-gradient-to-r ${card.color}`}>
|
||||||
|
<card.icon className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<span className={`text-sm font-medium ${
|
||||||
|
card.change.startsWith('+') ? 'text-green-600' : 'text-red-600'
|
||||||
|
}`}>
|
||||||
|
{card.change}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||||
|
{card.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{card.value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
dashboard/components/TopContactsTable.tsx
Normal file
88
dashboard/components/TopContactsTable.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Users, TrendingUp, TrendingDown } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function TopContactsTable() {
|
||||||
|
// Dados de exemplo - substituir por dados reais da API
|
||||||
|
const topContacts = [
|
||||||
|
{ name: 'João Silva', phone: '+55 11 99999-1234', messages: 1245, trend: 'up', change: 12 },
|
||||||
|
{ name: 'Maria Santos', phone: '+55 21 98888-5678', messages: 982, trend: 'up', change: 8 },
|
||||||
|
{ name: 'Pedro Oliveira', phone: '+55 31 97777-9012', messages: 856, trend: 'down', change: -3 },
|
||||||
|
{ name: 'Ana Costa', phone: '+55 41 96666-3456', messages: 734, trend: 'up', change: 15 },
|
||||||
|
{ name: 'Carlos Souza', phone: '+55 51 95555-7890', messages: 628, trend: 'up', change: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Users className="w-5 h-5 text-primary-500" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Top Contatos
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button className="text-sm text-primary-500 hover:text-primary-600 font-medium">
|
||||||
|
Ver todos
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<th className="text-left py-3 px-2 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Contato
|
||||||
|
</th>
|
||||||
|
<th className="text-right py-3 px-2 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Mensagens
|
||||||
|
</th>
|
||||||
|
<th className="text-right py-3 px-2 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Tendência
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{topContacts.map((contact, index) => (
|
||||||
|
<tr key={index} className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<td className="py-4 px-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900 dark:text-white">
|
||||||
|
{contact.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{contact.phone}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-4 px-2 text-right">
|
||||||
|
<span className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{contact.messages.toLocaleString('pt-BR')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-4 px-2 text-right">
|
||||||
|
<div className="flex items-center justify-end space-x-1">
|
||||||
|
{contact.trend === 'up' ? (
|
||||||
|
<>
|
||||||
|
<TrendingUp className="w-4 h-4 text-green-500" />
|
||||||
|
<span className="text-sm font-medium text-green-500">
|
||||||
|
+{contact.change}%
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<TrendingDown className="w-4 h-4 text-red-500" />
|
||||||
|
<span className="text-sm font-medium text-red-500">
|
||||||
|
{contact.change}%
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
dashboard/lib/prisma.ts
Normal file
15
dashboard/lib/prisma.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const globalForPrisma = globalThis as unknown as {
|
||||||
|
prisma: PrismaClient | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prisma =
|
||||||
|
globalForPrisma.prisma ??
|
||||||
|
new PrismaClient({
|
||||||
|
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
7
dashboard/next.config.js
Normal file
7
dashboard/next.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
swcMinify: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
37
dashboard/package.json
Normal file
37
dashboard/package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "evolution-dashboard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Painel Interativo com Chat para Evolution API",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "^14.2.0",
|
||||||
|
"react": "^18.3.0",
|
||||||
|
"react-dom": "^18.3.0",
|
||||||
|
"@prisma/client": "^6.1.0",
|
||||||
|
"recharts": "^2.12.7",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
|
"lucide-react": "^0.428.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"tailwind-merge": "^2.5.0",
|
||||||
|
"sentiment": "^5.0.2",
|
||||||
|
"natural": "^8.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.14.0",
|
||||||
|
"@types/react": "^18.3.0",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
|
"postcss": "^8.4.0",
|
||||||
|
"autoprefixer": "^10.4.0",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-next": "^14.2.0",
|
||||||
|
"prisma": "^6.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
dashboard/postcss.config.js
Normal file
6
dashboard/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
80
dashboard/prisma/schema.prisma
Normal file
80
dashboard/prisma/schema.prisma
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Instance {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
name String @unique
|
||||||
|
connectionStatus String?
|
||||||
|
ownerJid String?
|
||||||
|
profilePictureUrl String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
Message Message[]
|
||||||
|
Contact Contact[]
|
||||||
|
Chat Chat[]
|
||||||
|
|
||||||
|
@@map("Instance")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Message {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
key Json
|
||||||
|
pushName String?
|
||||||
|
participant String?
|
||||||
|
message Json
|
||||||
|
messageType String
|
||||||
|
messageTimestamp BigInt
|
||||||
|
instanceId String @db.Uuid
|
||||||
|
source String?
|
||||||
|
fromMe Boolean @default(false)
|
||||||
|
status String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([instanceId])
|
||||||
|
@@index([messageTimestamp])
|
||||||
|
@@index([fromMe])
|
||||||
|
@@map("Message")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Contact {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
remoteJid String
|
||||||
|
pushName String?
|
||||||
|
profilePictureUrl String?
|
||||||
|
instanceId String @db.Uuid
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([instanceId, remoteJid])
|
||||||
|
@@index([instanceId])
|
||||||
|
@@map("Contact")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Chat {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
remoteJid String
|
||||||
|
name String?
|
||||||
|
unreadMessages Int @default(0)
|
||||||
|
lastMessageTime BigInt?
|
||||||
|
instanceId String @db.Uuid
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([instanceId, remoteJid])
|
||||||
|
@@index([instanceId])
|
||||||
|
@@map("Chat")
|
||||||
|
}
|
||||||
29
dashboard/tailwind.config.ts
Normal file
29
dashboard/tailwind.config.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f0fdf4',
|
||||||
|
100: '#dcfce7',
|
||||||
|
200: '#bbf7d0',
|
||||||
|
300: '#86efac',
|
||||||
|
400: '#4ade80',
|
||||||
|
500: '#22c55e',
|
||||||
|
600: '#16a34a',
|
||||||
|
700: '#15803d',
|
||||||
|
800: '#166534',
|
||||||
|
900: '#14532d',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
27
dashboard/tsconfig.json
Normal file
27
dashboard/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user