init: scripts diversos (crawlers, conversores, scrapers)

This commit is contained in:
2026-03-05 20:38:36 +00:00
commit 6ac6f4be2a
925 changed files with 850330 additions and 0 deletions

View 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;