init: scripts diversos (crawlers, conversores, scrapers)
This commit is contained in:
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>
|
||||
);
|
||||
Reference in New Issue
Block a user