init: scripts diversos (crawlers, conversores, scrapers)
This commit is contained in:
25
email-cleaner/.gitignore
vendored
Executable file
25
email-cleaner/.gitignore
vendored
Executable file
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.vercel
|
||||
112
email-cleaner/App.tsx
Executable file
112
email-cleaner/App.tsx
Executable file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* App.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Header from './components/Header';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import Accounts from './components/Accounts';
|
||||
import Settings from './components/Settings';
|
||||
import type { Account, CrmSettings } from './types';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [theme, setTheme] = useState(() => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const userPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
return savedTheme || (userPrefersDark ? 'dark' : 'light');
|
||||
});
|
||||
|
||||
const [activeView, setActiveView] = useState('dashboard');
|
||||
const [accounts, setAccounts] = useState<Account[]>([
|
||||
{ id: 1, email: 'usuario@gmail.com', provider: 'gmail', avatar: 'https://i.pravatar.cc/40?img=3' }
|
||||
]);
|
||||
const [crmSettings, setCrmSettings] = useState<CrmSettings>(() => {
|
||||
const savedSettings = localStorage.getItem('crmSettings');
|
||||
const defaults = {
|
||||
url: '',
|
||||
token: '',
|
||||
autoDeleteTickets: false,
|
||||
};
|
||||
return savedSettings ? { ...defaults, ...JSON.parse(savedSettings) } : defaults;
|
||||
});
|
||||
|
||||
const [isAutoCleanEnabled, setIsAutoCleanEnabled] = useState<boolean>(() => {
|
||||
const savedState = localStorage.getItem('autoCleanEnabled');
|
||||
return savedState ? JSON.parse(savedState) : false;
|
||||
});
|
||||
|
||||
const [apiKey, setApiKey] = useState<string>(() => localStorage.getItem('geminiApiKey') || '');
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
if (theme === 'dark') {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('autoCleanEnabled', JSON.stringify(isAutoCleanEnabled));
|
||||
}, [isAutoCleanEnabled]);
|
||||
|
||||
|
||||
const handleSaveCrmSettings = (settings: CrmSettings) => {
|
||||
setCrmSettings(settings);
|
||||
localStorage.setItem('crmSettings', JSON.stringify(settings));
|
||||
};
|
||||
|
||||
const handleSaveApiKey = (key: string) => {
|
||||
setApiKey(key);
|
||||
localStorage.setItem('geminiApiKey', key);
|
||||
};
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
|
||||
};
|
||||
|
||||
const addAccount = (account: Omit<Account, 'id' | 'avatar'>) => {
|
||||
const newAccount: Account = {
|
||||
id: Date.now(),
|
||||
...account,
|
||||
avatar: `https://i.pravatar.cc/40?img=${Math.floor(Math.random() * 70)}`
|
||||
};
|
||||
setAccounts(prev => [...prev, newAccount]);
|
||||
};
|
||||
|
||||
const removeAccount = (accountId: number) => {
|
||||
setAccounts(prev => prev.filter(acc => acc.id !== accountId));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen text-slate-800 dark:text-slate-200">
|
||||
<Header theme={theme} toggleTheme={toggleTheme} />
|
||||
<div className="flex">
|
||||
<Sidebar activeView={activeView} setActiveView={setActiveView} />
|
||||
<main className="flex-1 p-4 sm:p-6 lg:p-8 bg-slate-50 dark:bg-slate-900">
|
||||
{activeView === 'dashboard' && <Dashboard crmSettings={crmSettings} isAutoCleanEnabled={isAutoCleanEnabled} apiKey={apiKey} />}
|
||||
{activeView === 'contas' && <Accounts accounts={accounts} addAccount={addAccount} removeAccount={removeAccount} />}
|
||||
{activeView === 'configuracoes' &&
|
||||
<Settings
|
||||
crmSettings={crmSettings}
|
||||
onSaveCrmSettings={handleSaveCrmSettings}
|
||||
isAutoCleanEnabled={isAutoCleanEnabled}
|
||||
setAutoCleanEnabled={setIsAutoCleanEnabled}
|
||||
apiKey={apiKey}
|
||||
onSaveApiKey={handleSaveApiKey}
|
||||
/>
|
||||
}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
80
email-cleaner/README.md
Executable file
80
email-cleaner/README.md
Executable file
@@ -0,0 +1,80 @@
|
||||
# Limpador de E-mail com IA - Guia de Implementação Backend
|
||||
|
||||
Este guia explica como implementar a automação de backend para que a limpeza de e-mails funcione 24/7, utilizando Google Cloud Run e Cloud Scheduler.
|
||||
|
||||
## Arquitetura
|
||||
|
||||
A aplicação agora consiste em duas partes principais:
|
||||
|
||||
1. **Frontend (Interface do Usuário):** O que você vê e interage no navegador. Usado para configurar, visualizar resultados e acionar análises manuais.
|
||||
2. **Backend (Lógica de Automação):** Um serviço sem servidor (`api/scan.ts`) que contém a lógica para analisar e-mails e limpar tickets. Este serviço é projetado para ser executado no Google Cloud Run.
|
||||
3. **Cloud Scheduler (Gatilho):** Um serviço da Google Cloud que aciona (chama) o nosso backend em intervalos regulares (a cada 2 horas).
|
||||
|
||||
---
|
||||
|
||||
## Passo 1: Implementar o Backend no Google Cloud Run
|
||||
|
||||
1. **Pré-requisitos:**
|
||||
* Tenha uma conta no [Google Cloud Platform](https://cloud.google.com/) com faturação ativa.
|
||||
* Instale e configure o [Google Cloud SDK (gcloud CLI)](https://cloud.google.com/sdk/docs/install).
|
||||
|
||||
2. **Crie um arquivo `package.json`** na raiz do seu projeto com o seguinte conteúdo. Isto é necessário para que o Cloud Run saiba como iniciar o seu serviço.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "email-cleaner-backend",
|
||||
"version": "1.0.0",
|
||||
"main": "api/scan.js",
|
||||
"scripts": {
|
||||
"start": "node api/scan.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.16.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
*Nota: Você precisará compilar o `api/scan.ts` para JavaScript (`api/scan.js`) antes de implementar, ou usar um builder que suporte TypeScript no Cloud Run.*
|
||||
|
||||
3. **Implemente o Serviço:**
|
||||
* Abra o terminal na raiz do seu projeto.
|
||||
* Execute o seguinte comando para implementar a função que está em `api/scan.ts`:
|
||||
|
||||
```bash
|
||||
gcloud run deploy email-cleaner-service --source . --region=us-central1 --allow-unauthenticated
|
||||
```
|
||||
* Siga as instruções no terminal. O comando `--allow-unauthenticated` é usado para permitir que o Cloud Scheduler chame o seu serviço. Em produção, você pode configurar um método de autenticação mais seguro.
|
||||
|
||||
4. **Configure as Variáveis de Ambiente:**
|
||||
* Após a implementação, vá para o [console do Cloud Run](https://console.cloud.google.com/run).
|
||||
* Encontre o seu serviço (`email-cleaner-service`) e clique nele.
|
||||
* Clique em "Editar e Implementar Nova Revisão".
|
||||
* Vá para a aba **"Variáveis e Segredos"**.
|
||||
* Adicione as seguintes variáveis de ambiente com os seus valores correspondentes:
|
||||
* `GEMINI_API_KEY`: A sua chave da API do Google Gemini.
|
||||
* `PERFEX_CRM_URL`: A URL da sua API do Perfex CRM.
|
||||
* `PERFEX_CRM_TOKEN`: O seu token da API do Perfex CRM.
|
||||
* `PERFEX_AUTO_DELETE_TICKETS`: `true` ou `false`.
|
||||
|
||||
---
|
||||
|
||||
## Passo 2: Configurar o Google Cloud Scheduler
|
||||
|
||||
1. **Vá para o Cloud Scheduler:**
|
||||
* No console do Google Cloud, navegue para a secção [Cloud Scheduler](https://console.cloud.google.com/cloudscheduler).
|
||||
|
||||
2. **Crie uma Tarefa (Job):**
|
||||
* Clique em "Criar Tarefa".
|
||||
* **Nome:** Dê um nome, por exemplo, `run-email-scan`.
|
||||
* **Frequência:** Defina a frequência usando a sintaxe cron. Para "a cada 2 horas", use: `0 */2 * * *`.
|
||||
* **Fuso Horário:** Selecione o seu fuso horário.
|
||||
* **Destino:** Selecione `HTTP`.
|
||||
* **URL:** Cole a URL do seu serviço Cloud Run que foi gerada no passo anterior.
|
||||
* **Método HTTP:** Selecione `POST`.
|
||||
* **Corpo:** Deixe em branco.
|
||||
* Clique em "Criar".
|
||||
|
||||
---
|
||||
|
||||
## Conclusão
|
||||
|
||||
É isso! Agora, o Cloud Scheduler irá chamar o seu serviço no Cloud Run a cada 2 horas. O serviço Cloud Run irá então executar a lógica de análise de e-mails e limpeza de tickets do CRM, de forma totalmente automática e independente do seu navegador.
|
||||
80
email-cleaner/api/scan.ts
Executable file
80
email-cleaner/api/scan.ts
Executable file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* scan.ts
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
// Este arquivo representa o seu backend sem servidor (Serverless Function).
|
||||
// Você deve implementá-lo em uma plataforma como Google Cloud Run, Vercel ou Netlify Functions.
|
||||
// As credenciais (API Key, CRM Token) devem ser configuradas como variáveis de ambiente na sua plataforma de hospedagem.
|
||||
|
||||
import { classifyEmails } from '../services/geminiService';
|
||||
import { deleteTicketsFromSendersInPerfex } from '../services/perfexCrmService';
|
||||
import { mockEmails } from '../data/mockEmails';
|
||||
import { EmailCategory, type CrmSettings } from '../types';
|
||||
|
||||
// Em um ambiente de servidor real, você precisaria de um framework como Express
|
||||
// para lidar com requisições HTTP. Esta é uma simulação para fins de demonstração.
|
||||
// A função `handleRequest` simula o ponto de entrada da sua função na nuvem.
|
||||
|
||||
async function handleRequest() {
|
||||
console.log('Iniciando tarefa de limpeza automática...');
|
||||
|
||||
// 1. Ler configurações das variáveis de ambiente
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
const crmSettings: CrmSettings = {
|
||||
url: process.env.PERFEX_CRM_URL || '',
|
||||
token: process.env.PERFEX_CRM_TOKEN || '',
|
||||
autoDeleteTickets: process.env.PERFEX_AUTO_DELETE_TICKETS === 'true',
|
||||
};
|
||||
|
||||
// Validação básica
|
||||
if (!apiKey) {
|
||||
console.error('Erro: A variável de ambiente GEMINI_API_KEY não está definida.');
|
||||
return { success: false, message: 'Chave da API do Gemini não configurada no servidor.' };
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Classificar e-mails usando o serviço Gemini
|
||||
const categorizedEmails = await classifyEmails(mockEmails, apiKey);
|
||||
console.log('E-mails classificados com sucesso.');
|
||||
|
||||
// 3. Excluir tickets do CRM, se configurado
|
||||
if (crmSettings.autoDeleteTickets && crmSettings.url && crmSettings.token) {
|
||||
const spamAndNotificationEmails = [
|
||||
...(categorizedEmails[EmailCategory.SPAM]?.emails || []),
|
||||
...(categorizedEmails[EmailCategory.NOTIFICATIONS]?.emails || [])
|
||||
];
|
||||
|
||||
const senderEmails = [...new Set(spamAndNotificationEmails.map(email => email.sender))];
|
||||
|
||||
if (senderEmails.length > 0) {
|
||||
try {
|
||||
const deletedCount = await deleteTicketsFromSendersInPerfex(crmSettings, senderEmails);
|
||||
console.log(`${deletedCount} tickets foram excluídos do CRM.`);
|
||||
} catch (err) {
|
||||
console.error('Erro ao excluir tickets do CRM:', (err as Error).message);
|
||||
}
|
||||
} else {
|
||||
console.log('Nenhum e-mail de spam ou notificação encontrado para exclusão de tickets.');
|
||||
}
|
||||
} else {
|
||||
console.log('A exclusão automática de tickets está desativada ou o CRM não está configurado.');
|
||||
}
|
||||
|
||||
console.log('Tarefa de limpeza automática concluída com sucesso.');
|
||||
return { success: true, message: 'Limpeza automática executada com sucesso.' };
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ocorreu um erro durante a execução da tarefa de limpeza automática:', error);
|
||||
return { success: false, message: 'Falha na execução da limpeza automática.' };
|
||||
}
|
||||
}
|
||||
|
||||
// Para simular a execução, você pode chamar a função.
|
||||
// Numa implementação real, o seu provedor de nuvem (ex: Cloud Run) chamaria
|
||||
// uma função exportada quando o endpoint HTTP fosse acionado.
|
||||
// Ex: `export default async (req, res) => { ... }`
|
||||
handleRequest();
|
||||
88
email-cleaner/api/test-imap.ts
Executable file
88
email-cleaner/api/test-imap.ts
Executable file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* test-imap.ts
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
// Em uma plataforma como Vercel ou Netlify, este arquivo se tornaria um endpoint de API.
|
||||
// Ex: POST /api/test-imap
|
||||
|
||||
import { ImapFlow } from 'imapflow';
|
||||
import type { ImapAccount } from '../types';
|
||||
|
||||
// Esta função simula o handler de uma requisição de API.
|
||||
// Em um ambiente real com Express, por exemplo, seria algo como:
|
||||
// export default async (req: Request, res: Response) => { ... }
|
||||
// Aqui, vamos simular o corpo da função.
|
||||
|
||||
const handler = async (account: Omit<ImapAccount, 'provider' | 'id' | 'avatar'>) => {
|
||||
const { host, port, user, password, secure } = account;
|
||||
|
||||
if (!host || !port || !user || !password) {
|
||||
// No mundo real, retornaríamos um status HTTP 400
|
||||
return {
|
||||
success: false,
|
||||
message: 'Todos os campos (host, porta, usuário, senha) são obrigatórios.',
|
||||
};
|
||||
}
|
||||
|
||||
const client = new ImapFlow({
|
||||
host,
|
||||
port,
|
||||
secure,
|
||||
auth: {
|
||||
user,
|
||||
pass: password,
|
||||
},
|
||||
logger: false, // Mude para true para debug detalhado no console do servidor
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
await client.logout();
|
||||
|
||||
// No mundo real, retornaríamos um status HTTP 200
|
||||
return {
|
||||
success: true,
|
||||
message: 'Conexão IMAP bem-sucedida!',
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Falha na conexão IMAP para ${user}@${host}:`, err);
|
||||
|
||||
let friendlyMessage = 'Falha na conexão IMAP. Verifique suas credenciais e configurações.';
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes('ENOTFOUND')) {
|
||||
friendlyMessage = 'O host do servidor não foi encontrado. Verifique o endereço do host.';
|
||||
} else if (err.message.includes('ECONNREFUSED')) {
|
||||
friendlyMessage = 'A conexão foi recusada. Verifique a porta e as configurações de segurança (TLS/SSL).';
|
||||
} else if (err.message.toLowerCase().includes('authentication failed')) {
|
||||
friendlyMessage = 'Falha na autenticação. Verifique o usuário e a senha.';
|
||||
}
|
||||
}
|
||||
|
||||
// No mundo real, retornaríamos um status HTTP 500 ou 401
|
||||
return {
|
||||
success: false,
|
||||
message: friendlyMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Para fins de teste, você pode simular uma chamada.
|
||||
// Em um projeto real, o framework (Express/Next/Vercel) chamaria o handler
|
||||
// com base no corpo da requisição (req.body).
|
||||
//
|
||||
// Exemplo de como o frontend chamaria esta API:
|
||||
//
|
||||
// const response = await fetch('/api/test-imap', {
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify({ host, port, user, password, secure })
|
||||
// });
|
||||
// const result = await response.json();
|
||||
// alert(result.message);
|
||||
|
||||
export default handler;
|
||||
94
email-cleaner/components/Accounts.tsx
Executable file
94
email-cleaner/components/Accounts.tsx
Executable file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Accounts.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import type { Account } from '../types';
|
||||
import AddAccountModal from './AddAccountModal';
|
||||
import { GmailIcon } from './icons/GmailIcon';
|
||||
import { OutlookIcon } from './icons/OutlookIcon';
|
||||
import { YahooIcon } from './icons/YahooIcon';
|
||||
import { ImapIcon } from './icons/ImapIcon';
|
||||
|
||||
interface AccountsProps {
|
||||
accounts: Account[];
|
||||
addAccount: (account: Omit<Account, 'id' | 'avatar'>) => void;
|
||||
}
|
||||
|
||||
const ProviderIcon = ({ provider }: { provider: Account['provider'] }) => {
|
||||
switch (provider) {
|
||||
case 'gmail':
|
||||
return <GmailIcon className="w-6 h-6" />;
|
||||
case 'outlook':
|
||||
return <OutlookIcon className="w-6 h-6" />;
|
||||
case 'yahoo':
|
||||
return <YahooIcon className="w-6 h-6" />;
|
||||
case 'imap':
|
||||
return <ImapIcon className="w-6 h-6" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const Accounts: React.FC<AccountsProps> = ({ accounts, addAccount }) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleAddAccount = (account: Omit<Account, 'id' | 'avatar'>) => {
|
||||
addAccount(account);
|
||||
setIsModalOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Contas Conectadas</h2>
|
||||
<p className="text-slate-500 dark:text-slate-400">Gerencie as contas de e-mail que você deseja limpar.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="bg-sky-500 text-white font-semibold py-2 px-5 rounded-lg hover:bg-sky-600 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
Adicionar Nova Conta
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{accounts.map(account => (
|
||||
<div key={account.id} className="bg-white dark:bg-slate-800 rounded-xl shadow-md p-5 border border-slate-200 dark:border-slate-700 flex items-center gap-4">
|
||||
<img src={account.avatar} alt={`Avatar de ${account.email}`} className="w-12 h-12 rounded-full" />
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-slate-700 dark:text-slate-200 truncate">{account.email}</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 capitalize flex items-center gap-2">
|
||||
<ProviderIcon provider={account.provider} />
|
||||
{account.provider}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => removeAccount(account.id)} className="p-2 rounded-full text-slate-400 hover:bg-slate-100 hover:text-red-500 dark:hover:bg-slate-700 transition-colors" aria-label="Remover conta">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.134-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.067-2.09 1.02-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddAccountModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onAddAccount={handleAddAccount}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Accounts;
|
||||
103
email-cleaner/components/AddAccountModal.tsx
Executable file
103
email-cleaner/components/AddAccountModal.tsx
Executable file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* AddAccountModal.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import type { Account } from '../types';
|
||||
import { GmailIcon } from './icons/GmailIcon';
|
||||
import { OutlookIcon } from './icons/OutlookIcon';
|
||||
import { YahooIcon } from './icons/YahooIcon';
|
||||
import { ImapIcon } from './icons/ImapIcon';
|
||||
import AddImapAccountForm from './AddImapAccountForm';
|
||||
|
||||
interface AddAccountModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onAddAccount: (account: Omit<Account, 'id' | 'avatar'>) => void;
|
||||
}
|
||||
|
||||
const AddAccountModal: React.FC<AddAccountModalProps> = ({ isOpen, onClose, onAddAccount }) => {
|
||||
const [view, setView] = useState<'providers' | 'imapForm'>('providers');
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleProviderClick = (provider: Account['provider']) => {
|
||||
const email = `novo.usuario.${Math.floor(Math.random() * 1000)}@${provider}.com`;
|
||||
onAddAccount({ email, provider });
|
||||
};
|
||||
|
||||
const handleImapConnect = (email: string) => {
|
||||
onAddAccount({ email, provider: 'imap' });
|
||||
}
|
||||
|
||||
const providers: { name: Account['provider'], icon: React.ReactNode }[] = [
|
||||
{ name: 'gmail', icon: <GmailIcon className="w-6 h-6" /> },
|
||||
{ name: 'outlook', icon: <OutlookIcon className="w-6 h-6" /> },
|
||||
{ name: 'yahoo', icon: <YahooIcon className="w-6 h-6 text-[#6001d2]" /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="bg-white dark:bg-slate-800 rounded-xl shadow-2xl p-6 w-full max-w-sm transform transition-all"
|
||||
role="document"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white" id="modal-title">
|
||||
{view === 'providers' ? 'Conectar nova conta' : 'Conectar Conta IMAP'}
|
||||
</h3>
|
||||
<button onClick={onClose} className="p-1 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6 text-slate-500">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{view === 'providers' ? (
|
||||
<>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mb-6">
|
||||
Selecione seu provedor de e-mail para conectar sua conta com segurança.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{providers.map(provider => (
|
||||
<button
|
||||
key={provider.name}
|
||||
onClick={() => handleProviderClick(provider.name)}
|
||||
className="w-full flex items-center gap-4 p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors"
|
||||
>
|
||||
{provider.icon}
|
||||
<span className="font-medium text-slate-700 dark:text-slate-200 capitalize">
|
||||
Conectar com {provider.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={() => setView('imapForm')}
|
||||
className="w-full flex items-center gap-4 p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors"
|
||||
>
|
||||
<ImapIcon className="w-6 h-6 text-slate-500" />
|
||||
<span className="font-medium text-slate-700 dark:text-slate-200">
|
||||
Conectar via IMAP
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<AddImapAccountForm onConnect={handleImapConnect} onBack={() => setView('providers')} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAccountModal;
|
||||
127
email-cleaner/components/AddImapAccountForm.tsx
Executable file
127
email-cleaner/components/AddImapAccountForm.tsx
Executable file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* AddImapAccountForm.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Spinner from './Spinner'; // Certifique-se que este componente exista
|
||||
|
||||
interface AddImapAccountFormProps {
|
||||
onConnect: (email: string, user: string, host: string, port: number, secure: boolean) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const AddImapAccountForm: React.FC<AddImapAccountFormProps> = ({ onConnect, onBack }) => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [user, setUser] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [host, setHost] = useState('');
|
||||
const [port, setPort] = useState(993);
|
||||
const [secure, setSecure] = useState(true);
|
||||
|
||||
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle');
|
||||
const [testMessage, setTestMessage] = useState('');
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
setTestStatus('testing');
|
||||
setTestMessage('');
|
||||
|
||||
// A API que criamos espera um corpo de requisição (body).
|
||||
// Como estamos em um ambiente de desenvolvimento sem um servidor Node rodando,
|
||||
// não podemos chamar a API diretamente. A lógica abaixo é como o frontend
|
||||
// se comportaria se estivesse fazendo um fetch para /api/test-imap.
|
||||
// Por enquanto, vamos simular a chamada e focar na UI.
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/test-imap', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ host, port, user, password, secure })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Tenta extrair uma mensagem de erro amigável do corpo da resposta
|
||||
const errorResult = await response.json().catch(() => null);
|
||||
throw new Error(errorResult?.message || `Erro no servidor: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setTestStatus('success');
|
||||
setTestMessage(result.message);
|
||||
} else {
|
||||
setTestStatus('error');
|
||||
setTestMessage(result.message || 'Ocorreu um erro desconhecido.');
|
||||
}
|
||||
} catch (error) {
|
||||
setTestStatus('error');
|
||||
setTestMessage(error instanceof Error ? error.message : 'Falha ao se comunicar com o servidor de teste.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (testStatus === 'success') {
|
||||
onConnect(email, user, host, port, secure);
|
||||
} else {
|
||||
alert('Por favor, teste a conexão com sucesso antes de adicionar a conta.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Campos do formulário... */}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-slate-700 dark:text-slate-300">Endereço de E-mail (para exibição)</label>
|
||||
<input type="email" id="email" value={email} onChange={(e) => setEmail(e.target.value)} required className="mt-1 block w-full input" placeholder="seunome@dominio.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="user" className="block text-sm font-medium text-slate-700 dark:text-slate-300">Nome de Usuário (para login)</label>
|
||||
<input type="text" id="user" value={user} onChange={(e) => setUser(e.target.value)} required className="mt-1 block w-full input" placeholder="geralmente o mesmo que o e-mail" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-slate-700 dark:text-slate-300">Senha</label>
|
||||
<input type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} required className="mt-1 block w-full input" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2">
|
||||
<label htmlFor="host" className="block text-sm font-medium text-slate-700 dark:text-slate-300">Servidor IMAP</label>
|
||||
<input type="text" id="host" value={host} onChange={(e) => setHost(e.target.value)} required className="mt-1 block w-full input" placeholder="imap.dominio.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="port" className="block text-sm font-medium text-slate-700 dark:text-slate-300">Porta</label>
|
||||
<input type="number" id="port" value={port} onChange={(e) => setPort(parseInt(e.target.value))} required className="mt-1 block w-full input" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input type="checkbox" id="secure" checked={secure} onChange={(e) => setSecure(e.target.checked)} className="h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500" />
|
||||
<label htmlFor="secure" className="ml-2 block text-sm text-slate-900 dark:text-slate-300">Usar conexão segura (SSL/TLS)</label>
|
||||
</div>
|
||||
|
||||
{testMessage && (
|
||||
<div className={`text-sm p-3 rounded-md ${testStatus === 'success' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{testMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between gap-3 pt-2">
|
||||
<button type="button" onClick={onBack} className="btn-secondary">Voltar</button>
|
||||
<div className="flex items-center gap-3">
|
||||
<button type="button" onClick={handleTestConnection} disabled={testStatus === 'testing'} className="btn-secondary flex items-center gap-2">
|
||||
{testStatus === 'testing' && <Spinner className="w-4 h-4" />}
|
||||
{testStatus === 'testing' ? 'Testando...' : 'Testar Conexão'}
|
||||
</button>
|
||||
<button type="submit" disabled={testStatus !== 'success'} className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Adicionar Conta
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddImapAccountForm;
|
||||
8
email-cleaner/components/AutoCleanManager.tsx
Executable file
8
email-cleaner/components/AutoCleanManager.tsx
Executable file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* AutoCleanManager.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
79
email-cleaner/components/ConfirmationModal.tsx
Executable file
79
email-cleaner/components/ConfirmationModal.tsx
Executable file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* ConfirmationModal.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface ConfirmationModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
categoryName: string;
|
||||
emailCount: number;
|
||||
}
|
||||
|
||||
const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
categoryName,
|
||||
emailCount,
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
>
|
||||
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-2xl p-6 w-full max-w-md transform transition-all"
|
||||
role="document"
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/50 mb-4">
|
||||
<svg className="h-6 w-6 text-red-600 dark:text-red-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white" id="modal-title">
|
||||
Confirmar Limpeza
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
Você tem certeza que deseja remover permanentemente{' '}
|
||||
<span className="font-bold text-slate-700 dark:text-slate-200">{emailCount} e-mails</span> da categoria{' '}
|
||||
<span className="font-bold text-slate-700 dark:text-slate-200">{categoryName}</span>?
|
||||
</p>
|
||||
<p className="text-xs text-slate-400 dark:text-slate-500 mt-1">
|
||||
Esta ação não pode ser desfeita.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full justify-center rounded-md border border-slate-300 dark:border-slate-600 px-4 py-2 bg-white dark:bg-slate-800 text-base font-medium text-slate-700 dark:text-slate-200 shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 dark:focus:ring-offset-slate-900 sm:text-sm"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full justify-center rounded-md border border-transparent px-4 py-2 bg-red-600 text-base font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 dark:focus:ring-offset-slate-900 sm:text-sm"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Sim, Limpar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationModal;
|
||||
208
email-cleaner/components/Dashboard.tsx
Executable file
208
email-cleaner/components/Dashboard.tsx
Executable file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Dashboard.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { mockEmails } from '../data/mockEmails';
|
||||
import { classifyEmails } from '../services/geminiService';
|
||||
import { deleteTicketsFromSendersInPerfex } from '../services/perfexCrmService';
|
||||
import type { CategorizedEmails, CrmSettings } from '../types';
|
||||
import { EmailCategory } from '../types';
|
||||
import EmailCategoryCard from './EmailCategoryCard';
|
||||
import Spinner from './Spinner';
|
||||
import ConfirmationModal from './ConfirmationModal';
|
||||
|
||||
interface DashboardProps {
|
||||
crmSettings: CrmSettings;
|
||||
isAutoCleanEnabled: boolean;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
const Dashboard: React.FC<DashboardProps> = ({ crmSettings, isAutoCleanEnabled, apiKey }) => {
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
const [categorizedEmails, setCategorizedEmails] = useState<CategorizedEmails | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
|
||||
const [categoryToDelete, setCategoryToDelete] = useState<{name: string; count: number} | null>(null);
|
||||
|
||||
const [ticketDeletionStatus, setTicketDeletionStatus] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
|
||||
|
||||
const handleScanInbox = useCallback(async () => {
|
||||
setCategorizedEmails(null);
|
||||
setIsScanning(true);
|
||||
setError(null);
|
||||
setTicketDeletionStatus(null);
|
||||
|
||||
try {
|
||||
const result = await classifyEmails(mockEmails, apiKey);
|
||||
setCategorizedEmails(result);
|
||||
|
||||
if (crmSettings.autoDeleteTickets && crmSettings.url && crmSettings.token) {
|
||||
const spamAndNotificationEmails = [
|
||||
...(result[EmailCategory.SPAM]?.emails || []),
|
||||
...(result[EmailCategory.NOTIFICATIONS]?.emails || [])
|
||||
];
|
||||
|
||||
const senderEmails = [...new Set(spamAndNotificationEmails.map(email => email.sender))];
|
||||
|
||||
if (senderEmails.length > 0) {
|
||||
try {
|
||||
const deletedCount = await deleteTicketsFromSendersInPerfex(crmSettings, senderEmails);
|
||||
if (deletedCount > 0) {
|
||||
setTicketDeletionStatus({ message: `${deletedCount} tickets de spam/notificações foram excluídos do CRM.`, type: 'success' });
|
||||
}
|
||||
} catch (err) {
|
||||
setTicketDeletionStatus({ message: (err as Error).message, type: 'error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Falha ao analisar a caixa de entrada. A chave de API do Gemini pode estar ausente ou inválida.');
|
||||
} finally {
|
||||
setIsScanning(false);
|
||||
}
|
||||
}, [crmSettings, apiKey]);
|
||||
|
||||
const handleClearCategory = (category: string, emailCount: number) => {
|
||||
setCategoryToDelete({ name: category, count: emailCount });
|
||||
setIsConfirmModalOpen(true);
|
||||
};
|
||||
|
||||
const confirmClearCategory = () => {
|
||||
if(!categorizedEmails || !categoryToDelete) return;
|
||||
const newCategories = { ...categorizedEmails };
|
||||
delete newCategories[categoryToDelete.name];
|
||||
setCategorizedEmails(newCategories);
|
||||
setIsConfirmModalOpen(false);
|
||||
setCategoryToDelete(null);
|
||||
};
|
||||
|
||||
const getGreeting = () => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) return 'Bom dia';
|
||||
if (hour < 18) return 'Boa tarde';
|
||||
return 'Boa noite';
|
||||
}
|
||||
|
||||
const TicketDeletionToast = () => {
|
||||
if (!ticketDeletionStatus) return null;
|
||||
const isSuccess = ticketDeletionStatus.type === 'success';
|
||||
return (
|
||||
<div className={`${isSuccess ? 'bg-green-100 dark:bg-green-900/50 border-green-500 text-green-700 dark:text-green-300' : 'bg-red-100 dark:bg-red-900/50 border-red-500 text-red-700 dark:text-red-300'} border-l-4 p-4 rounded-md flex justify-between items-center`} role="alert">
|
||||
<p>{ticketDeletionStatus.message}</p>
|
||||
<button onClick={() => setTicketDeletionStatus(null)} className="p-1 rounded-full hover:bg-black/10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">{getGreeting()}, Usuário!</h2>
|
||||
<p className="text-slate-500 dark:text-slate-400">
|
||||
{isAutoCleanEnabled
|
||||
? 'A limpeza automática está ativa e a ser executada no servidor.'
|
||||
: 'Vamos organizar sua caixa de entrada.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TicketDeletionToast />
|
||||
|
||||
{isAutoCleanEnabled ? (
|
||||
<div className="p-4 bg-sky-50 dark:bg-sky-900/50 rounded-xl border border-sky-200 dark:border-sky-800 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div className="text-sm">
|
||||
<p className="font-semibold text-sky-600 dark:text-sky-300">Automação Ativa</p>
|
||||
<p className="text-slate-600 dark:text-slate-300 mt-1">
|
||||
Sua caixa de entrada é verificada em segundo plano a cada 2 horas.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleScanInbox()}
|
||||
disabled={isScanning}
|
||||
className="bg-sky-500 text-white font-semibold py-2 px-5 rounded-lg hover:bg-sky-600 transition-colors disabled:bg-sky-300 disabled:cursor-not-allowed flex items-center gap-2 flex-shrink-0"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={`w-5 h-5 ${isScanning ? 'animate-spin' : ''}`}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001a7.5 7.5 0 0 1-1.06 3.58a7.5 7.5 0 0 1-4.225 3.58a7.5 7.5 0 0 1-5.232 0a7.5 7.5 0 0 1-4.225-3.58a7.5 7.5 0 0 1-1.06-3.58m7.5 0v-4.5m0 4.5v4.5m0-4.5h4.5m-4.5 0h-4.5" />
|
||||
</svg>
|
||||
{isScanning ? 'Analisando...' : 'Forçar Análise Agora'}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-6 bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-800 dark:text-white">Pronto para começar?</h3>
|
||||
<p className="text-slate-500 dark:text-slate-400 mt-1">Clique para que a IA analise e organize sua caixa de entrada.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleScanInbox()}
|
||||
disabled={isScanning}
|
||||
className="bg-sky-500 text-white font-semibold py-3 px-6 rounded-lg hover:bg-sky-600 transition-colors disabled:bg-sky-300 flex items-center gap-2 flex-shrink-0"
|
||||
>
|
||||
{isScanning ? 'Analisando...' : 'Analisar Caixa de Entrada'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isScanning && !categorizedEmails &&(
|
||||
<div className="flex flex-col items-center justify-center p-10 bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700">
|
||||
<Spinner />
|
||||
<p className="mt-4 text-slate-600 dark:text-slate-300 animate-pulse">A IA está analisando seus e-mails...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-100 dark:bg-red-900/50 border-l-4 border-red-500 text-red-700 dark:text-red-300 p-4 rounded-md" role="alert">
|
||||
<p className="font-bold">Oops! Algo deu errado.</p>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{categorizedEmails && Object.keys(categorizedEmails).length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{Object.entries(categorizedEmails).map(([category, data]) => (
|
||||
<EmailCategoryCard
|
||||
key={category}
|
||||
category={category}
|
||||
summary={data.summary}
|
||||
emails={data.emails}
|
||||
onClear={() => handleClearCategory(category, data.emails.length)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{categorizedEmails && Object.keys(categorizedEmails).length === 0 && !isScanning && (
|
||||
<div className="text-center p-10 bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700">
|
||||
<div className="mx-auto w-16 h-16 bg-green-100 dark:bg-green-900/50 rounded-full flex items-center justify-center mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8 text-green-500">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-slate-800 dark:text-white">Caixa de entrada limpa!</h3>
|
||||
<p className="text-slate-500 dark:text-slate-400 mt-1 mb-6">Você limpou todos os e-mails acionáveis.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConfirmationModal
|
||||
isOpen={isConfirmModalOpen}
|
||||
onClose={() => setIsConfirmModalOpen(false)}
|
||||
onConfirm={confirmClearCategory}
|
||||
categoryName={categoryToDelete?.name || ''}
|
||||
emailCount={categoryToDelete?.count || 0}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
85
email-cleaner/components/EmailCategoryCard.tsx
Executable file
85
email-cleaner/components/EmailCategoryCard.tsx
Executable file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* EmailCategoryCard.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import type { Email } from '../types';
|
||||
import { EmailCategory } from '../types';
|
||||
|
||||
interface EmailCategoryCardProps {
|
||||
category: string;
|
||||
summary: string;
|
||||
emails: Email[];
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
const categoryStyles: { [key in EmailCategory]?: { icon: string; bg: string; text: string; } } = {
|
||||
[EmailCategory.PROMOTIONS]: { icon: '🛍️', bg: 'bg-indigo-100 dark:bg-indigo-900/50', text: 'text-indigo-600 dark:text-indigo-300' },
|
||||
[EmailCategory.NOTIFICATIONS]: { icon: '🔔', bg: 'bg-amber-100 dark:bg-amber-900/50', text: 'text-amber-600 dark:text-amber-300' },
|
||||
[EmailCategory.NEWSLETTERS]: { icon: '📰', bg: 'bg-cyan-100 dark:bg-cyan-900/50', text: 'text-cyan-600 dark:text-cyan-300' },
|
||||
[EmailCategory.SPAM]: { icon: '🗑️', bg: 'bg-red-100 dark:bg-red-900/50', text: 'text-red-600 dark:text-red-300' },
|
||||
};
|
||||
|
||||
const EmailCategoryCard: React.FC<EmailCategoryCardProps> = ({ category, summary, emails, onClear }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setIsVisible(true), 10);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const styles = categoryStyles[category as EmailCategory] || { icon: '✉️', bg: 'bg-slate-100 dark:bg-slate-700', text: 'text-slate-600 dark:text-slate-300' };
|
||||
|
||||
if (emails.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={`bg-white dark:bg-slate-800 rounded-xl shadow-md border border-slate-200 dark:border-slate-700 overflow-hidden transition-all duration-500 hover:shadow-lg hover:-translate-y-1 ${isVisible ? 'opacity-100' : 'opacity-0'}`}>
|
||||
<div className="p-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`text-2xl ${styles.bg} p-2 rounded-md`}>{styles.icon}</span>
|
||||
<h3 className="text-lg font-bold text-slate-800 dark:text-white capitalize">{category.toLowerCase()}</h3>
|
||||
</div>
|
||||
<p className="text-slate-500 dark:text-slate-400 text-sm mt-2">{summary}</p>
|
||||
</div>
|
||||
<span className={`${styles.bg} ${styles.text} text-sm font-bold px-3 py-1 rounded-full`}>
|
||||
{emails.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<button onClick={onClear} className="w-full bg-red-500 text-white font-semibold py-2 px-4 rounded-lg hover:bg-red-600 transition-colors flex items-center justify-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.134-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.067-2.09 1.02-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
Limpar {emails.length} e-mails
|
||||
</button>
|
||||
<button onClick={() => setIsExpanded(!isExpanded)} className="w-full mt-2 text-sm text-slate-500 dark:text-slate-400 hover:text-sky-500 dark:hover:text-sky-400">
|
||||
{isExpanded ? 'Ocultar e-mails' : 'Rever e-mails'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="bg-slate-50 dark:bg-slate-800/50 p-4 border-t border-slate-200 dark:border-slate-700 max-h-60 overflow-y-auto">
|
||||
<ul className="space-y-3">
|
||||
{emails.map(email => (
|
||||
<li key={email.id} className="text-sm p-2 rounded-md bg-white dark:bg-slate-700/50">
|
||||
<p className="font-semibold text-slate-700 dark:text-slate-200 truncate">{email.sender}</p>
|
||||
<p className="text-slate-500 dark:text-slate-400 truncate">{email.subject}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailCategoryCard;
|
||||
55
email-cleaner/components/Header.tsx
Executable file
55
email-cleaner/components/Header.tsx
Executable file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Header.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import { MailIcon } from './icons/MailIcon';
|
||||
|
||||
interface HeaderProps {
|
||||
theme: string;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ theme, toggleTheme }) => {
|
||||
return (
|
||||
<header className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-sm p-4 flex justify-between items-center border-b border-slate-200 dark:border-slate-700 sticky top-0 z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-sky-500 p-2 rounded-lg">
|
||||
<MailIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-slate-800 dark:text-white">Limpador de E-mail com IA</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
||||
aria-label={theme === 'dark' ? 'Ativar modo claro' : 'Ativar modo escuro'}
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button className="p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-slate-500 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
</button>
|
||||
<img src="https://i.pravatar.cc/40?img=3" alt="Avatar do usuário" className="w-10 h-10 rounded-full" />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
8
email-cleaner/components/RegisterInvoicesModal.tsx
Executable file
8
email-cleaner/components/RegisterInvoicesModal.tsx
Executable file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* RegisterInvoicesModal.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
195
email-cleaner/components/Settings.tsx
Executable file
195
email-cleaner/components/Settings.tsx
Executable file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Settings.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import type { CrmSettings } from '../types';
|
||||
|
||||
interface SettingsProps {
|
||||
crmSettings: CrmSettings;
|
||||
onSaveCrmSettings: (settings: CrmSettings) => void;
|
||||
isAutoCleanEnabled: boolean;
|
||||
setAutoCleanEnabled: (enabled: boolean) => void;
|
||||
apiKey: string;
|
||||
onSaveApiKey: (key: string) => void;
|
||||
}
|
||||
|
||||
const Settings: React.FC<SettingsProps> = ({
|
||||
crmSettings,
|
||||
onSaveCrmSettings,
|
||||
isAutoCleanEnabled,
|
||||
setAutoCleanEnabled,
|
||||
apiKey,
|
||||
onSaveApiKey
|
||||
}) => {
|
||||
const [currentCrmSettings, setCurrentCrmSettings] = useState<CrmSettings>(crmSettings);
|
||||
const [currentApiKey, setCurrentApiKey] = useState<string>(apiKey);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentCrmSettings(crmSettings);
|
||||
}, [crmSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentApiKey(apiKey);
|
||||
}, [apiKey]);
|
||||
|
||||
const handleCrmChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
setCurrentCrmSettings(prev => ({
|
||||
...prev,
|
||||
[name]: type === 'checkbox' ? checked : value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSaveCrmSettings(currentCrmSettings);
|
||||
onSaveApiKey(currentApiKey);
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => setShowSuccess(false), 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-3xl mx-auto">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Configurações</h2>
|
||||
<p className="text-slate-500 dark:text-slate-400">Gerencie as integrações e preferências da aplicação.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-md border border-slate-200 dark:border-slate-700">
|
||||
<div className="p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-800 dark:text-white">Limpeza Automática (Backend)</h3>
|
||||
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">
|
||||
Ative para que um processo no servidor (Cloud Run) analise seus e-mails a cada 2 horas, mesmo com o navegador fechado.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-slate-50 dark:bg-slate-800/50 p-6 border-t border-slate-200 dark:border-slate-700 rounded-b-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-slate-700 dark:text-slate-200">
|
||||
{isAutoCleanEnabled ? 'Automação Ativada' : 'Automação Desativada'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setAutoCleanEnabled(!isAutoCleanEnabled)}
|
||||
className={`${
|
||||
isAutoCleanEnabled ? 'bg-sky-500' : 'bg-slate-300 dark:bg-slate-600'
|
||||
} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 dark:ring-offset-slate-800`}
|
||||
role="switch"
|
||||
aria-checked={isAutoCleanEnabled}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
isAutoCleanEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-3">
|
||||
<strong>Nota:</strong> Para a automação funcionar, as chaves de API abaixo devem ser configuradas como **variáveis de ambiente** no seu serviço Cloud Run. Consulte o arquivo `README.md`.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-md border border-slate-200 dark:border-slate-700 mb-8">
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-800 dark:text-white">Chave da API do Google Gemini</h3>
|
||||
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">Insira sua chave para habilitar a análise com IA nas verificações manuais.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="apiKey" className="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
Sua Chave API
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="apiKey"
|
||||
id="apiKey"
|
||||
value={currentApiKey}
|
||||
onChange={(e) => setCurrentApiKey(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
|
||||
placeholder="Cole sua chave da API do Gemini aqui"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-md border border-slate-200 dark:border-slate-700">
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-800 dark:text-white">Integração com Perfex CRM</h3>
|
||||
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">Conecte-se à sua instância do Perfex CRM para automatizar tarefas.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="url" className="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
URL da API do Perfex CRM
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
name="url"
|
||||
id="url"
|
||||
value={currentCrmSettings.url}
|
||||
onChange={handleCrmChange}
|
||||
className="mt-1 block w-full px-3 py-2 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
|
||||
placeholder="https://seu-crm.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="token" className="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
Token da API
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="token"
|
||||
id="token"
|
||||
value={currentCrmSettings.token}
|
||||
onChange={handleCrmChange}
|
||||
className="mt-1 block w-full px-3 py-2 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
|
||||
placeholder="Seu token de API seguro"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex h-5 items-center">
|
||||
<input
|
||||
id="autoDeleteTickets"
|
||||
name="autoDeleteTickets"
|
||||
type="checkbox"
|
||||
checked={currentCrmSettings.autoDeleteTickets}
|
||||
onChange={handleCrmChange}
|
||||
className="h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="autoDeleteTickets" className="font-medium text-slate-700 dark:text-slate-200">
|
||||
Excluir Tickets de Spam/Notificações
|
||||
</label>
|
||||
<p className="text-slate-500 dark:text-slate-400">Excluir automaticamente tickets do CRM criados por e-mails de spam ou notificações.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 dark:bg-slate-800/50 p-4 border-t border-slate-200 dark:border-slate-700 rounded-b-xl flex items-center justify-end gap-4">
|
||||
{showSuccess && <p className="text-sm text-green-600 dark:text-green-400 animate-pulse">Configurações salvas com sucesso!</p>}
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-sky-500 text-white font-semibold py-2 px-5 rounded-lg hover:bg-sky-600 transition-colors"
|
||||
>
|
||||
Salvar Todas as Configurações
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
56
email-cleaner/components/Sidebar.tsx
Executable file
56
email-cleaner/components/Sidebar.tsx
Executable file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Sidebar.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import { DashboardIcon } from './icons/DashboardIcon';
|
||||
import { SettingsIcon } from './icons/SettingsIcon';
|
||||
import { AccountIcon } from './icons/AccountIcon';
|
||||
import { HelpIcon } from './icons/HelpIcon';
|
||||
|
||||
interface SidebarProps {
|
||||
activeView: string;
|
||||
setActiveView: (view: string) => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ activeView, setActiveView }) => {
|
||||
|
||||
const navItems = [
|
||||
{ id: 'dashboard', name: 'Dashboard', icon: <DashboardIcon className="w-5 h-5" /> },
|
||||
{ id: 'contas', name: 'Contas', icon: <AccountIcon className="w-5 h-5" /> },
|
||||
{ id: 'configuracoes', name: 'Configurações', icon: <SettingsIcon className="w-5 h-5" /> },
|
||||
{ id: 'ajuda', name: 'Ajuda', icon: <HelpIcon className="w-5 h-5" /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-white dark:bg-slate-800/50 p-4 border-r border-slate-200 dark:border-slate-700 hidden md:block">
|
||||
<nav className="flex flex-col gap-2">
|
||||
{navItems.map(item => (
|
||||
<a
|
||||
key={item.id}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setActiveView(item.id);
|
||||
}}
|
||||
className={`flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeView === item.id
|
||||
? 'bg-sky-500 text-white'
|
||||
: 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
18
email-cleaner/components/Spinner.tsx
Executable file
18
email-cleaner/components/Spinner.tsx
Executable file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Spinner.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const Spinner: React.FC = () => {
|
||||
return (
|
||||
<div className="w-12 h-12 border-4 border-t-4 border-slate-200 dark:border-slate-600 border-t-sky-500 rounded-full animate-spin"></div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
16
email-cleaner/components/icons/AccountIcon.tsx
Executable file
16
email-cleaner/components/icons/AccountIcon.tsx
Executable file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* AccountIcon.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const AccountIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
);
|
||||
16
email-cleaner/components/icons/DashboardIcon.tsx
Executable file
16
email-cleaner/components/icons/DashboardIcon.tsx
Executable file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* DashboardIcon.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const DashboardIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h7.5" />
|
||||
</svg>
|
||||
);
|
||||
21
email-cleaner/components/icons/GmailIcon.tsx
Executable file
21
email-cleaner/components/icons/GmailIcon.tsx
Executable file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* GmailIcon.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const GmailIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path d="M22 6.5H2V17.5H22V6.5Z" fill="#4285F4"/>
|
||||
<path d="M2.22217 6.64185L12 14.5L21.7778 6.64185C21.6163 6.55198 21.4173 6.5 21.2001 6.5H2.80006C2.5828 6.5 2.38378 6.55198 2.22217 6.64185Z" fill="#34A853"/>
|
||||
<path d="M2 17.5L9.5 11.5L12 13.5L14.5 11.5L22 17.5V6.5L12 14.5L2 6.5V17.5Z" fill="#EA4335"/>
|
||||
<path d="M2 17.5H22V16.5L12 8.5L2 16.5V17.5Z" fill="#FBBC05"/>
|
||||
<path d="M22 6.5V17.5L14.5 11.5L22 6.5Z" fill="#C5221F"/>
|
||||
<path d="M2 6.5V17.5L9.5 11.5L2 6.5Z" fill="#1E8E3E"/>
|
||||
</svg>
|
||||
);
|
||||
16
email-cleaner/components/icons/HelpIcon.tsx
Executable file
16
email-cleaner/components/icons/HelpIcon.tsx
Executable file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* HelpIcon.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const HelpIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
|
||||
</svg>
|
||||
);
|
||||
15
email-cleaner/components/icons/ImapIcon.tsx
Executable file
15
email-cleaner/components/icons/ImapIcon.tsx
Executable file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* ImapIcon.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const ImapIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
|
||||
</svg>
|
||||
);
|
||||
16
email-cleaner/components/icons/MailIcon.tsx
Executable file
16
email-cleaner/components/icons/MailIcon.tsx
Executable file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* MailIcon.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const MailIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21.75 9v.906a2.25 2.25 0 0 1-1.183 1.981l-6.478 3.488a2.25 2.25 0 0 1-2.18 0l-6.478-3.488A2.25 2.25 0 0 1 2.25 9.906V9m19.5 0a2.25 2.25 0 0 0-2.25-2.25H4.5A2.25 2.25 0 0 0 2.25 9m19.5 0v.906a2.25 2.25 0 0 1-1.183 1.981l-6.478 3.488a2.25 2.25 0 0 1-2.18 0l-6.478-3.488A2.25 2.25 0 0 1 2.25 9.906V9" />
|
||||
</svg>
|
||||
);
|
||||
18
email-cleaner/components/icons/OutlookIcon.tsx
Executable file
18
email-cleaner/components/icons/OutlookIcon.tsx
Executable file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* OutlookIcon.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const OutlookIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path d="M14 3H4C3.44772 3 3 3.44772 3 4V20C3 20.5523 3.44772 21 4 21H14C14.5523 21 15 20.5523 15 20V4C15 3.44772 14.5523 3 14 3Z" fill="#0072C6"/>
|
||||
<path d="M20.5 7.5H15V16.5H20.5C20.7761 16.5 21 16.2761 21 16V8C21 7.72386 20.7761 7.5 20.5 7.5Z" fill="#0072C6"/>
|
||||
<path d="M15 8.5V15.5L18.5 12L15 8.5Z" fill="white"/>
|
||||
</svg>
|
||||
);
|
||||
16
email-cleaner/components/icons/SettingsIcon.tsx
Executable file
16
email-cleaner/components/icons/SettingsIcon.tsx
Executable file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* SettingsIcon.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const SettingsIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75" />
|
||||
</svg>
|
||||
);
|
||||
16
email-cleaner/components/icons/YahooIcon.tsx
Executable file
16
email-cleaner/components/icons/YahooIcon.tsx
Executable file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* YahooIcon.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const YahooIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" {...props}>
|
||||
<path d="M12.001 2C6.476 2 2 6.476 2 12s4.476 10 10.001 10C17.525 22 22 17.524 22 12S17.525 2 12.001 2zm3.33 13.064h-2.112l-2.08-4.116h-.032l-.56 4.116H8.441L7.1 6.936h2.128l1.6 4.316h.048l2.256-4.316h1.968l-2.32 3.84 2.553 5.288z"/>
|
||||
</svg>
|
||||
);
|
||||
110
email-cleaner/data/mockEmails.ts
Executable file
110
email-cleaner/data/mockEmails.ts
Executable file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* mockEmails.ts
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
import type { Email } from '../types';
|
||||
|
||||
export const mockEmails: Email[] = [
|
||||
{
|
||||
id: 1,
|
||||
sender: 'GitHub',
|
||||
subject: '[github/project] Pull request #123: New feature',
|
||||
snippet: 'Hey, a new pull request has been submitted by user...',
|
||||
timestamp: '2023-10-27T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
sender: 'Loja de Roupas',
|
||||
subject: '🔥 50% de Desconto em Toda a Loja! Não perca!',
|
||||
snippet: 'As melhores ofertas da estação estão aqui. Clique para aproveitar...',
|
||||
timestamp: '2023-10-27T09:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
sender: 'TechCrunch Daily',
|
||||
subject: 'As Últimas Notícias de Tecnologia',
|
||||
snippet: 'Apple anuncia novo chip, startup de IA levanta $100M...',
|
||||
timestamp: '2023-10-27T09:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
sender: 'LinkedIn',
|
||||
subject: 'Você apareceu em 15 pesquisas esta semana',
|
||||
snippet: 'Veja quem está interessado no seu perfil profissional.',
|
||||
timestamp: '2023-10-27T08:45:00Z',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
sender: 'iFood',
|
||||
subject: 'Seu almoço com R$15 de desconto',
|
||||
snippet: 'Peça agora e aproveite este cupom exclusivo para você.',
|
||||
timestamp: '2023-10-27T08:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
sender: 'Medium Digest',
|
||||
subject: 'Histórias recomendadas para você',
|
||||
snippet: 'Descubra novos artigos sobre programação, design e produtividade.',
|
||||
timestamp: '2023-10-27T08:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
sender: 'Google Agenda',
|
||||
subject: 'Notificação: Reunião de Equipe às 11:00',
|
||||
snippet: 'Lembrete do seu próximo evento agendado.',
|
||||
timestamp: '2023-10-27T07:55:00Z',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
sender: 'Casas Bahia',
|
||||
subject: 'Ofertas Imperdíveis de Eletrônicos!',
|
||||
snippet: 'Smart TVs, smartphones e notebooks com preços incríveis.',
|
||||
timestamp: '2023-10-26T22:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
sender: 'Nerd-Letter',
|
||||
subject: 'Resumo da Semana no Mundo Geek',
|
||||
snippet: 'Novos trailers, reviews de jogos e muito mais.',
|
||||
timestamp: '2023-10-26T20:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
sender: 'Asana',
|
||||
subject: 'A tarefa "Finalizar relatório" vence hoje',
|
||||
snippet: 'Lembre-se de completar suas tarefas pendentes.',
|
||||
timestamp: '2023-10-26T18:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
sender: 'Amazon.com.br',
|
||||
subject: 'Produtos que você pode gostar',
|
||||
snippet: 'Baseado em suas últimas compras, encontramos itens para você.',
|
||||
timestamp: '2023-10-26T15:10:00Z',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
sender: 'The New York Times',
|
||||
subject: 'Your Morning Briefing',
|
||||
snippet: 'Here\'s what you need to know today...',
|
||||
timestamp: '2023-10-26T12:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
sender: 'Facebook',
|
||||
subject: 'Você tem novas notificações',
|
||||
snippet: 'Maria comentou na sua foto.',
|
||||
timestamp: '2023-10-26T11:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
sender: 'UX Design Weekly',
|
||||
subject: 'Issue #354 of UX Design Weekly',
|
||||
snippet: 'A hand-picked list of the best user experience design links.',
|
||||
timestamp: '2023-10-26T10:00:00Z',
|
||||
}
|
||||
];
|
||||
26
email-cleaner/index.html
Executable file
26
email-cleaner/index.html
Executable file
@@ -0,0 +1,26 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Limpador de E-mail com IA</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react/": "https://aistudiocdn.com/react@^19.1.1/",
|
||||
"react": "https://aistudiocdn.com/react@^19.1.1",
|
||||
"react-dom/": "https://aistudiocdn.com/react-dom@^19.1.1/",
|
||||
"@google/genai": "https://aistudiocdn.com/@google/genai@^1.16.0"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body class="bg-slate-50 dark:bg-slate-900">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
email-cleaner/index.tsx
Executable file
24
email-cleaner/index.tsx
Executable file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* index.tsx
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
5
email-cleaner/metadata.json
Executable file
5
email-cleaner/metadata.json
Executable file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Limpador de E-mail com IA",
|
||||
"description": "Uma aplicação web inteligente que utiliza a IA do Gemini para analisar, categorizar e ajudar a limpar sua caixa de entrada de e-mails promocionais e notificações inúteis, mantendo apenas o que é importante.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
1686
email-cleaner/package-lock.json
generated
Executable file
1686
email-cleaner/package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
22
email-cleaner/package.json
Executable file
22
email-cleaner/package.json
Executable file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "limpador-de-e-mail-com-ia",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.16.0",
|
||||
"imapflow": "^1.0.195",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
112
email-cleaner/services/geminiService.ts
Executable file
112
email-cleaner/services/geminiService.ts
Executable file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* geminiService.ts
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
import { GoogleGenAI, Type } from "@google/genai";
|
||||
import type { Email, ClassifiedCategory, CategorizedEmails } from "../types";
|
||||
import { EmailCategory } from "../types";
|
||||
|
||||
const MOCKED_RESPONSE: ClassifiedCategory[] = [
|
||||
{
|
||||
category: EmailCategory.PROMOTIONS,
|
||||
summary: "Ofertas especiais e descontos de várias lojas online.",
|
||||
email_ids: [2, 5, 8, 11]
|
||||
},
|
||||
{
|
||||
category: EmailCategory.NOTIFICATIONS,
|
||||
summary: "Alertas de login, atualizações de projetos e menções em redes sociais.",
|
||||
email_ids: [1, 4, 7, 10, 13]
|
||||
},
|
||||
{
|
||||
category: EmailCategory.NEWSLETTERS,
|
||||
summary: "Boletins informativos de tecnologia, design e notícias gerais.",
|
||||
email_ids: [3, 6, 9, 12, 14]
|
||||
}
|
||||
];
|
||||
|
||||
const processApiResponse = (
|
||||
response: ClassifiedCategory[],
|
||||
allEmails: Email[]
|
||||
): CategorizedEmails => {
|
||||
const categorized: CategorizedEmails = {};
|
||||
|
||||
for (const item of response) {
|
||||
const categoryKey = item.category;
|
||||
|
||||
if (categoryKey !== EmailCategory.IMPORTANT && categoryKey !== EmailCategory.UNKNOWN) {
|
||||
const emailsInCategory = item.email_ids.map(id =>
|
||||
allEmails.find(email => email.id === id)
|
||||
).filter((email): email is Email => email !== undefined);
|
||||
|
||||
if(emailsInCategory.length > 0) {
|
||||
if (!categorized[categoryKey]) {
|
||||
categorized[categoryKey] = { summary: item.summary, emails: [] };
|
||||
}
|
||||
categorized[categoryKey].summary = item.summary;
|
||||
categorized[categoryKey].emails.push(...emailsInCategory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return categorized;
|
||||
};
|
||||
|
||||
export const classifyEmails = async (emails: Email[], apiKey: string): Promise<CategorizedEmails> => {
|
||||
if (!apiKey) {
|
||||
console.warn("API_KEY for Gemini is not provided. Using mocked data.");
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
return processApiResponse(MOCKED_RESPONSE, emails);
|
||||
}
|
||||
|
||||
const ai = new GoogleGenAI({ apiKey });
|
||||
|
||||
const emailPromptData = emails.map(e => ({
|
||||
id: e.id,
|
||||
from: e.sender,
|
||||
subject: e.subject,
|
||||
snippet: e.snippet
|
||||
}));
|
||||
|
||||
|
||||
const prompt = `
|
||||
Você é um especialista em organização de e-mails. Analise a seguinte lista de e-mails em formato JSON.
|
||||
1. Categorize cada e-mail em uma das seguintes categorias base: ${Object.values(EmailCategory).join(', ')}.
|
||||
2. Agrupe os e-mails por categoria. Para cada categoria, forneça um breve resumo em português do conteúdo e uma lista dos IDs dos e-mails correspondentes.
|
||||
3. NÃO inclua a categoria 'IMPORTANTE' no resultado final.
|
||||
|
||||
Aqui está a lista de e-mails:
|
||||
${JSON.stringify(emailPromptData, null, 2)}
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model: "gemini-2.5-flash",
|
||||
contents: prompt,
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
category: { type: Type.STRING },
|
||||
summary: { type: Type.STRING },
|
||||
email_ids: { type: Type.ARRAY, items: { type: Type.INTEGER } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const classifiedData = JSON.parse(response.text);
|
||||
return processApiResponse(classifiedData, emails);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error calling Gemini API:", error);
|
||||
throw new Error("Failed to classify emails with Gemini API.");
|
||||
}
|
||||
};
|
||||
34
email-cleaner/services/perfexCrmService.ts
Executable file
34
email-cleaner/services/perfexCrmService.ts
Executable file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* perfexCrmService.ts
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
import type { CrmSettings } from '../types';
|
||||
|
||||
/**
|
||||
* Simula a exclusão de tickets no Perfex CRM com base nos remetentes de e-mail.
|
||||
*/
|
||||
export const deleteTicketsFromSendersInPerfex = (
|
||||
settings: CrmSettings,
|
||||
senderEmails: string[]
|
||||
): Promise<number> => {
|
||||
console.log('Iniciando exclusão de tickets de spam/notificações no Perfex CRM...');
|
||||
console.log('Remetentes alvo:', senderEmails);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (!settings.url || !settings.token) {
|
||||
return reject(new Error('Não foi possível excluir tickets: URL do CRM ou Token não configurado.'));
|
||||
}
|
||||
if (settings.token === 'fail') {
|
||||
return reject(new Error('Falha ao excluir tickets no Perfex CRM.'));
|
||||
}
|
||||
const simulatedDeletedCount = Math.floor(Math.random() * senderEmails.length) + 1;
|
||||
console.log(`Sucesso! ${simulatedDeletedCount} tickets foram "excluídos" do Perfex CRM.`);
|
||||
resolve(simulatedDeletedCount);
|
||||
}, 1500);
|
||||
});
|
||||
};
|
||||
29
email-cleaner/tsconfig.json
Executable file
29
email-cleaner/tsconfig.json
Executable file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
50
email-cleaner/types.ts
Executable file
50
email-cleaner/types.ts
Executable file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* types.ts
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
export interface Email {
|
||||
id: number;
|
||||
sender: string;
|
||||
subject: string;
|
||||
snippet: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export enum EmailCategory {
|
||||
PROMOTIONS = 'PROMOÇÕES',
|
||||
NOTIFICATIONS = 'NOTIFICAÇÕES',
|
||||
IMPORTANT = 'IMPORTANTE',
|
||||
NEWSLETTERS = 'NEWSLETTERS',
|
||||
SPAM = 'SPAM',
|
||||
UNKNOWN = 'DESCONHECIDO'
|
||||
}
|
||||
|
||||
export interface ClassifiedCategory {
|
||||
category: EmailCategory | string;
|
||||
summary: string;
|
||||
email_ids: number[];
|
||||
}
|
||||
|
||||
export interface CategorizedEmails {
|
||||
[key: string]: {
|
||||
summary: string;
|
||||
emails: Email[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
id: number;
|
||||
email: string;
|
||||
provider: 'gmail' | 'outlook' | 'yahoo' | 'imap';
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export interface CrmSettings {
|
||||
url: string;
|
||||
token: string;
|
||||
autoDeleteTickets: boolean;
|
||||
}
|
||||
25
email-cleaner/vite.config.ts
Executable file
25
email-cleaner/vite.config.ts
Executable file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* vite.config.ts
|
||||
*
|
||||
* @author Descomplicar® Crescimento Digital
|
||||
* @link https://descomplicar.pt
|
||||
* @copyright 2025 Descomplicar®
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user