209 lines
9.4 KiB
TypeScript
Executable File
209 lines
9.4 KiB
TypeScript
Executable File
/**
|
|
* 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;
|