diff --git a/dashboard/.env.example b/dashboard/.env.example new file mode 100644 index 00000000..3539daf3 --- /dev/null +++ b/dashboard/.env.example @@ -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 diff --git a/dashboard/.eslintrc.json b/dashboard/.eslintrc.json new file mode 100644 index 00000000..957cd154 --- /dev/null +++ b/dashboard/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals"] +} diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 00000000..dca9f720 --- /dev/null +++ b/dashboard/.gitignore @@ -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 diff --git a/dashboard/QUICKSTART.md b/dashboard/QUICKSTART.md new file mode 100644 index 00000000..74a3483b --- /dev/null +++ b/dashboard/QUICKSTART.md @@ -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) diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 00000000..b7bfca36 --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,338 @@ +# 📊 Evolution Dashboard + +Painel interativo com IA para análise profunda de mensagens do WhatsApp via Evolution API. + +![Next.js](https://img.shields.io/badge/Next.js-14-black) +![TypeScript](https://img.shields.io/badge/TypeScript-5-blue) +![Tailwind CSS](https://img.shields.io/badge/Tailwind-3-38bdf8) +![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15-336791) + +## ✨ 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** diff --git a/dashboard/app/api/chat/route.ts b/dashboard/app/api/chat/route.ts new file mode 100644 index 00000000..64cebea1 --- /dev/null +++ b/dashboard/app/api/chat/route.ts @@ -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 } + ); + } +} diff --git a/dashboard/app/api/messages/route.ts b/dashboard/app/api/messages/route.ts new file mode 100644 index 00000000..265169a5 --- /dev/null +++ b/dashboard/app/api/messages/route.ts @@ -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 } + ); + } +} diff --git a/dashboard/app/api/sentiment/route.ts b/dashboard/app/api/sentiment/route.ts new file mode 100644 index 00000000..a4e231ff --- /dev/null +++ b/dashboard/app/api/sentiment/route.ts @@ -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 } + ); + } +} diff --git a/dashboard/app/api/stats/route.ts b/dashboard/app/api/stats/route.ts new file mode 100644 index 00000000..393c9724 --- /dev/null +++ b/dashboard/app/api/stats/route.ts @@ -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 } + ); + } +} diff --git a/dashboard/app/globals.css b/dashboard/app/globals.css new file mode 100644 index 00000000..fdbb5a9a --- /dev/null +++ b/dashboard/app/globals.css @@ -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); + } +} diff --git a/dashboard/app/layout.tsx b/dashboard/app/layout.tsx new file mode 100644 index 00000000..36f48c05 --- /dev/null +++ b/dashboard/app/layout.tsx @@ -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 ( + + {children} + + ); +} diff --git a/dashboard/app/page.tsx b/dashboard/app/page.tsx new file mode 100644 index 00000000..f4c47274 --- /dev/null +++ b/dashboard/app/page.tsx @@ -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 ( +
+ {/* Header */} +
+
+
+
+
+ +
+
+

+ Evolution Dashboard +

+

+ Análise Inteligente de Mensagens WhatsApp +

+
+
+ + {/* Tab Navigation */} +
+ + +
+
+
+
+ + {/* Content */} +
+ {activeTab === 'dashboard' ? : } +
+ + {/* Footer */} + +
+ ); +} diff --git a/dashboard/components/ChatInterface.tsx b/dashboard/components/ChatInterface.tsx new file mode 100644 index 00000000..a4bb183d --- /dev/null +++ b/dashboard/components/ChatInterface.tsx @@ -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([ + { + 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(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 ( +
+ {/* Chat Principal */} +
+
+ {/* Header */} +
+
+
+ +
+
+

+ Assistente IA +

+

+ Análise inteligente de dados +

+
+
+
+ + {/* Messages */} +
+ {messages.map((message) => ( +
+
+ {message.role === 'user' ? ( + + ) : ( + + )} +
+
+
+

{message.content}

+ + {message.timestamp.toLocaleTimeString('pt-BR', { + hour: '2-digit', + minute: '2-digit' + })} + +
+
+
+ ))} + + {loading && ( +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ )} + +
+
+ + {/* Input */} +
+
+ 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" + /> + +
+
+
+
+ + {/* Sugestões */} +
+
+

+ Perguntas Sugeridas +

+
+ {suggestedQuestions.map((q, index) => ( + + ))} +
+
+ +
+ +

+ Dica Pro +

+

+ Faça perguntas específicas para obter análises mais precisas. Você pode perguntar sobre períodos, contatos, sentimentos e muito mais! +

+
+
+
+ ); +} diff --git a/dashboard/components/Dashboard.tsx b/dashboard/components/Dashboard.tsx new file mode 100644 index 00000000..05aa8cae --- /dev/null +++ b/dashboard/components/Dashboard.tsx @@ -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 ( +
+
+
+ ); + } + + return ( +
+ {/* Filtros */} +
+
+
+ +

+ Filtros +

+
+
+ + +
+
+ +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+
+ + {/* Cards de Estatísticas */} + + + {/* Gráficos */} +
+ + +
+ + {/* Timeline e Top Contatos */} +
+ + +
+
+ ); +} diff --git a/dashboard/components/MessageChart.tsx b/dashboard/components/MessageChart.tsx new file mode 100644 index 00000000..33ac129a --- /dev/null +++ b/dashboard/components/MessageChart.tsx @@ -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 ( +
+
+
+ +

+ Mensagens por Dia +

+
+ Últimos 7 dias +
+ + + + + + + + + + + + +
+ ); +} diff --git a/dashboard/components/MessageTimeline.tsx b/dashboard/components/MessageTimeline.tsx new file mode 100644 index 00000000..e66ad3a9 --- /dev/null +++ b/dashboard/components/MessageTimeline.tsx @@ -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 ( +
+
+
+ +

+ Mensagens Recentes +

+
+ +
+ +
+ {recentMessages.map((msg) => ( +
+
+ +
+
+
+

+ {msg.contact} +

+ + {formatDistanceToNow(msg.time, { addSuffix: true, locale: ptBR })} + +
+

+ {msg.message} +

+ + {getSentimentLabel(msg.sentiment)} + +
+
+ ))} +
+
+ ); +} diff --git a/dashboard/components/SentimentChart.tsx b/dashboard/components/SentimentChart.tsx new file mode 100644 index 00000000..5e98f2bc --- /dev/null +++ b/dashboard/components/SentimentChart.tsx @@ -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 ( +
+
+
+ +

+ Análise de Sentimento +

+
+ + Total: {data.reduce((a, b) => a + b.value, 0).toLocaleString('pt-BR')} + +
+ + + + + {data.map((entry, index) => ( + + ))} + + value.toLocaleString('pt-BR')} + /> + {value}} + /> + + +
+ ); +} diff --git a/dashboard/components/StatsCards.tsx b/dashboard/components/StatsCards.tsx new file mode 100644 index 00000000..388564e0 --- /dev/null +++ b/dashboard/components/StatsCards.tsx @@ -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 ( +
+ {cards.map((card, index) => ( +
+
+
+ +
+ + {card.change} + +
+

+ {card.title} +

+

+ {card.value} +

+
+ ))} +
+ ); +} diff --git a/dashboard/components/TopContactsTable.tsx b/dashboard/components/TopContactsTable.tsx new file mode 100644 index 00000000..8597fb6a --- /dev/null +++ b/dashboard/components/TopContactsTable.tsx @@ -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 ( +
+
+
+ +

+ Top Contatos +

+
+ +
+ +
+ + + + + + + + + + {topContacts.map((contact, index) => ( + + + + + + ))} + +
+ Contato + + Mensagens + + Tendência +
+
+

+ {contact.name} +

+

+ {contact.phone} +

+
+
+ + {contact.messages.toLocaleString('pt-BR')} + + +
+ {contact.trend === 'up' ? ( + <> + + + +{contact.change}% + + + ) : ( + <> + + + {contact.change}% + + + )} +
+
+
+
+ ); +} diff --git a/dashboard/lib/prisma.ts b/dashboard/lib/prisma.ts new file mode 100644 index 00000000..c2a61982 --- /dev/null +++ b/dashboard/lib/prisma.ts @@ -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; diff --git a/dashboard/next.config.js b/dashboard/next.config.js new file mode 100644 index 00000000..3d3bc999 --- /dev/null +++ b/dashboard/next.config.js @@ -0,0 +1,7 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + swcMinify: true, +}; + +module.exports = nextConfig; diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 00000000..a05b7d9c --- /dev/null +++ b/dashboard/package.json @@ -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" + } +} diff --git a/dashboard/postcss.config.js b/dashboard/postcss.config.js new file mode 100644 index 00000000..12a703d9 --- /dev/null +++ b/dashboard/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/dashboard/prisma/schema.prisma b/dashboard/prisma/schema.prisma new file mode 100644 index 00000000..73c4ee2d --- /dev/null +++ b/dashboard/prisma/schema.prisma @@ -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") +} diff --git a/dashboard/tailwind.config.ts b/dashboard/tailwind.config.ts new file mode 100644 index 00000000..06367119 --- /dev/null +++ b/dashboard/tailwind.config.ts @@ -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; diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json new file mode 100644 index 00000000..2c145a2d --- /dev/null +++ b/dashboard/tsconfig.json @@ -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"] +}