import React, { useState, useRef, useEffect } from 'react'; import { Ticket, KBArticle, Agent, TicketStatus, TicketPriority, SurveyResult, AppSettings, ClientUser, TicketQueue, EmailTemplate, EmailTrigger, EmailAudience, AgentAvatarConfig, AgentRole, AiProvider } from '../types'; import { generateNewKBArticle } from '../services/geminiService'; import { ToastType } from './Toast'; import { LayoutDashboard, BookOpen, Users, Sparkles, CheckCircle, Clock, Edit3, Plus, ExternalLink, FileText, BarChart3, TrendingUp, MessageCircle, Star, Settings, Trash2, Mail, Palette, Shield, Save, LogOut, Paperclip, Layers, X, Camera, Move, Check, Zap, Copy, Activity, UserPlus, Loader2, Lock, Cpu, AlertTriangle, RotateCcw, Bot, Key, Archive, Inbox, Send, Eye, EyeOff, Globe, Sliders, ChevronRight } from 'lucide-react'; interface AgentDashboardProps { currentUser: Agent; tickets: Ticket[]; articles: KBArticle[]; agents: Agent[]; queues: TicketQueue[]; surveys?: SurveyResult[]; clientUsers: ClientUser[]; settings: AppSettings; updateTicketStatus: (id: string, status: TicketStatus) => void; updateTicketAgent: (id: string, agentId: string) => void; onReplyTicket: (ticketId: string, message: string) => void; addArticle: (article: KBArticle) => void; updateArticle: (article: KBArticle) => void; deleteArticle?: (id: string) => void; markTicketsAsAnalyzed?: (ticketIds: string[]) => void; addAgent: (agent: Agent) => void; updateAgent: (agent: Agent) => void; removeAgent: (id: string) => void; addClientUser: (user: ClientUser) => void; updateClientUser: (user: ClientUser) => void; removeClientUser: (id: string) => void; updateSettings: (settings: AppSettings) => void; addQueue: (queue: TicketQueue) => void; removeQueue: (id: string) => void; onLogout: () => void; showToast: (message: string, type: ToastType) => void; } // --- SUB-COMPONENT: Avatar Editor --- interface AvatarEditorProps { initialImage: string; initialConfig?: AgentAvatarConfig; onSave: (image: string, config: AgentAvatarConfig) => void; } const AvatarEditor: React.FC = ({ initialImage, initialConfig, onSave }) => { const [image, setImage] = useState(initialImage); const [config, setConfig] = useState(initialConfig || { x: 50, y: 50, scale: 1 }); const [isDragging, setIsDragging] = useState(false); const dragStart = useRef<{x: number, y: number}>({ x: 0, y: 0 }); useEffect(() => { setImage(initialImage); if(initialConfig) setConfig(initialConfig); }, [initialImage, initialConfig]); const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { const url = URL.createObjectURL(e.target.files[0]); setImage(url); setConfig({ x: 50, y: 50, scale: 1 }); } }; const handleMouseDown = (e: React.MouseEvent) => { setIsDragging(true); dragStart.current = { x: e.clientX, y: e.clientY }; }; const handleMouseMove = (e: React.MouseEvent) => { if (!isDragging) return; const deltaX = (e.clientX - dragStart.current.x) * 0.5; const deltaY = (e.clientY - dragStart.current.y) * 0.5; setConfig(prev => ({ ...prev, x: Math.min(100, Math.max(0, prev.x - deltaX * 0.2)), y: Math.min(100, Math.max(0, prev.y - deltaY * 0.2)) })); dragStart.current = { x: e.clientX, y: e.clientY }; }; const handleMouseUp = () => setIsDragging(false); return (
{image ? ( Avatar ) : (
)}
{image && (
Zoom setConfig({...config, scale: parseFloat(e.target.value)})} className="flex-1 h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-brand-600" />
)}

Trascina per centrare

); }; // --- MAIN COMPONENT --- export const AgentDashboard: React.FC = ({ currentUser, tickets, articles, agents, queues, surveys = [], clientUsers, settings, updateTicketStatus, updateTicketAgent, onReplyTicket, addArticle, updateArticle, deleteArticle, markTicketsAsAnalyzed, addAgent, updateAgent, removeAgent, addClientUser, updateClientUser, removeClientUser, updateSettings, addQueue, removeQueue, onLogout, showToast }) => { const [view, setView] = useState<'tickets' | 'kb' | 'ai' | 'analytics' | 'settings'>('tickets'); const [selectedTicketId, setSelectedTicketId] = useState(null); const [selectedQueue, setSelectedQueue] = useState(null); const [isViewingArchive, setIsViewingArchive] = useState(false); const [replyText, setReplyText] = useState(''); // ROLE BASED PERMISSIONS const canManageGlobalSettings = currentUser.role === 'superadmin'; const canManageTeam = currentUser.role === 'superadmin' || currentUser.role === 'supervisor'; const canAccessSettings = canManageTeam || canManageGlobalSettings; const [settingsTab, setSettingsTab] = useState<'general' | 'system' | 'ai' | 'users' | 'agents' | 'queues' | 'email'>('users'); const [isSaving, setIsSaving] = useState(false); useEffect(() => { if (view === 'settings') { if (canManageGlobalSettings) setSettingsTab('system'); else if (canManageTeam) setSettingsTab('users'); } }, [view, canManageGlobalSettings, canManageTeam]); useEffect(() => { if (view === 'tickets' && !selectedQueue && !isViewingArchive && queues.length > 0) { const firstAssignedQueue = queues.find(q => currentUser.queues.includes(q.name)); if (firstAssignedQueue) { setSelectedQueue(firstAssignedQueue.name); } else if (queues.length > 0) { setSelectedQueue(queues[0].name); } } }, [view, queues, currentUser]); // KB Editor State const [isEditingKB, setIsEditingKB] = useState(false); const [newArticle, setNewArticle] = useState>({ type: 'article', category: 'General', visibility: 'public' }); const [isFetchingUrl, setIsFetchingUrl] = useState(false); // AI State const [isAiAnalyzing, setIsAiAnalyzing] = useState(false); const [aiSuggestions, setAiSuggestions] = useState>([]); // Forms State for Settings const [newAgentForm, setNewAgentForm] = useState>({ name: '', email: '', password: '', skills: [], queues: [], role: 'agent', avatar: '', avatarConfig: {x: 50, y: 50, scale: 1} }); const [editingAgent, setEditingAgent] = useState(null); const [newUserForm, setNewUserForm] = useState>({ name: '', email: '', status: 'active', company: '' }); const [editingUser, setEditingUser] = useState(null); const [newQueueForm, setNewQueueForm] = useState>({ name: '', description: '' }); const [tempSettings, setTempSettings] = useState(settings); // Sync tempSettings with settings prop ONLY if NOT in settings view // This prevents background polling from resetting form data while the user is typing useEffect(() => { if (view !== 'settings') { setTempSettings(settings); } }, [settings, view]); const [isTestingSmtp, setIsTestingSmtp] = useState(false); const selectedTicket = tickets.find(t => t.id === selectedTicketId); // Stats for Quotas const currentAgents = agents.filter(a => a.role === 'agent').length; const currentSupervisors = agents.filter(a => a.role === 'supervisor').length; const currentArticles = articles.length; const currentAiArticles = articles.filter(a => a.source === 'ai').length; const isKbFull = currentArticles >= settings.features.maxKbArticles; const isAgentQuotaFull = currentAgents >= settings.features.maxAgents; const isSupervisorQuotaFull = currentSupervisors >= settings.features.maxSupervisors; const getAssignableAgents = (ticketQueue: string) => { if (canManageTeam) return agents; return agents.filter(a => a.queues.includes(ticketQueue)); }; const fetchUrlContent = async (url: string): Promise => { const parseContent = (html: string) => { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const scripts = doc.querySelectorAll('script, style, nav, footer, header, svg, noscript, iframe'); scripts.forEach(node => node.remove()); let text = doc.body?.textContent || ""; text = text.replace(/\s+/g, ' ').trim(); return text.substring(0, 5000); }; try { const res1 = await fetch(`https://api.allorigins.win/get?url=${encodeURIComponent(url)}`); if (res1.ok) { const data = await res1.json(); if (data.contents) return parseContent(data.contents); } } catch (e) { console.warn("Primary scraping failed, trying fallback..."); } try { const res2 = await fetch(`https://corsproxy.io/?${encodeURIComponent(url)}`); if (res2.ok) { const html = await res2.text(); return parseContent(html); } } catch (e) { console.error("Scraping error:", e); } return ""; }; // Handlers const handleReplySubmit = () => { if (selectedTicketId && replyText.trim()) { onReplyTicket(selectedTicketId, replyText); setReplyText(''); showToast('Risposta inviata', 'success'); } }; const handleAiAnalysis = async () => { if (!settings.features.aiKnowledgeAgentEnabled) { showToast("L'Agente Knowledge AI è disabilitato dall'amministratore.", 'error'); return; } if (currentAiArticles >= settings.features.maxAiGeneratedArticles) { showToast("Quota creazione articoli AI raggiunta. Impossibile generare nuovi suggerimenti.", 'error'); return; } if (!settings.aiConfig.apiKey) { showToast("Chiave API mancante. Configura l'AI nelle impostazioni.", 'error'); return; } setIsAiAnalyzing(true); setAiSuggestions([]); const suggestions = await generateNewKBArticle( settings.aiConfig.apiKey, tickets, articles, settings.aiConfig.provider, settings.aiConfig.model ); if (suggestions) { setAiSuggestions(suggestions); if (suggestions.length === 0) { showToast("Nessuna nuova lacuna identificata dall'AI nei ticket recenti.", 'info'); } const ticketsToMark = tickets .filter(t => t.status === TicketStatus.RESOLVED && !t.hasBeenAnalyzed) .map(t => t.id); if (ticketsToMark.length > 0 && markTicketsAsAnalyzed) { markTicketsAsAnalyzed(ticketsToMark); } } else { showToast("Errore durante l'analisi AI.", 'error'); } setIsAiAnalyzing(false); }; const saveAiArticle = (suggestion: { title: string; content: string; category: string }, index: number) => { addArticle({ id: `kb-${Date.now()}-${index}`, title: suggestion.title, content: suggestion.content, category: suggestion.category, type: 'article', source: 'ai', visibility: 'internal', lastUpdated: new Date().toISOString().split('T')[0] }); setAiSuggestions(prev => prev.filter((_, i) => i !== index)); showToast("Articolo aggiunto alla KB (Visibilità: Interna)", 'success'); }; const discardAiArticle = (index: number) => { setAiSuggestions(prev => prev.filter((_, i) => i !== index)); }; const handleSaveArticle = async () => { if (newArticle.title && (newArticle.content || newArticle.url)) { let finalContent = newArticle.content || ''; if (newArticle.type === 'url' && newArticle.url) { setIsFetchingUrl(true); const scrapedText = await fetchUrlContent(newArticle.url); setIsFetchingUrl(false); if (scrapedText) { finalContent = `[CONTENUTO PAGINA WEB SCARICATO]\n\n${scrapedText}\n\n[NOTE MANUALI]\n${newArticle.content || ''}`; } else { finalContent = newArticle.content || 'Contenuto non recuperabile automaticamente. Fare riferimento al link.'; } } const articleToSave: KBArticle = { id: newArticle.id || `kb-${Date.now()}`, title: newArticle.title, content: finalContent, category: newArticle.category || 'General', type: newArticle.type || 'article', url: newArticle.url, source: newArticle.source || 'manual', visibility: newArticle.visibility || 'public', lastUpdated: new Date().toISOString().split('T')[0] }; if (newArticle.id) { updateArticle(articleToSave); } else { addArticle(articleToSave); } setIsEditingKB(false); setNewArticle({ type: 'article', category: 'General', visibility: 'public' }); } }; const handleAvatarSaved = (image: string, config: AgentAvatarConfig, isEditing: boolean) => { if (isEditing && editingAgent) { setEditingAgent({ ...editingAgent, avatar: image, avatarConfig: config }); } else { setNewAgentForm({ ...newAgentForm, avatar: image, avatarConfig: config }); } }; const handleAddAgent = () => { if(newAgentForm.name && newAgentForm.email && newAgentForm.queues && newAgentForm.queues.length > 0) { addAgent({ id: `a${Date.now()}`, name: newAgentForm.name, email: newAgentForm.email, password: newAgentForm.password || 'password', role: newAgentForm.role || 'agent', avatar: newAgentForm.avatar || 'https://via.placeholder.com/200', avatarConfig: newAgentForm.avatarConfig, skills: newAgentForm.skills || ['General'], queues: newAgentForm.queues } as Agent); setNewAgentForm({ name: '', email: '', password: '', role: 'agent', skills: [], queues: [], avatar: '', avatarConfig: {x: 50, y: 50, scale: 1} }); } else { showToast("Compila nome, email e seleziona almeno una coda.", 'error'); } }; const handleUpdateAgent = () => { if (editingAgent && newAgentForm.name && newAgentForm.email) { const updatedAgent: Agent = { ...editingAgent, name: newAgentForm.name!, email: newAgentForm.email!, password: newAgentForm.password || editingAgent.password, role: newAgentForm.role as AgentRole, avatar: newAgentForm.avatar || editingAgent.avatar, avatarConfig: newAgentForm.avatarConfig, queues: newAgentForm.queues || [], skills: newAgentForm.skills || [] }; updateAgent(updatedAgent); setEditingAgent(null); setNewAgentForm({ name: '', email: '', password: '', role: 'agent', skills: [], queues: [], avatar: '', avatarConfig: {x: 50, y: 50, scale: 1} }); } }; const handleEditAgentClick = (agent: Agent) => { setEditingAgent(agent); setNewAgentForm({ name: agent.name, email: agent.email, password: agent.password, role: agent.role, avatar: agent.avatar, avatarConfig: agent.avatarConfig, skills: agent.skills, queues: agent.queues }); }; const cancelEditAgent = () => { setEditingAgent(null); setNewAgentForm({ name: '', email: '', password: '', role: 'agent', skills: [], queues: [], avatar: '', avatarConfig: {x: 50, y: 50, scale: 1} }); }; const toggleQueueInForm = (queueName: string, isEditing: boolean) => { const currentQueues = newAgentForm.queues || []; const newQueues = currentQueues.includes(queueName) ? currentQueues.filter(q => q !== queueName) : [...currentQueues, queueName]; setNewAgentForm({ ...newAgentForm, queues: newQueues }); }; const handleAddUser = () => { if(newUserForm.name && newUserForm.email) { addClientUser({ id: `u${Date.now()}`, name: newUserForm.name, email: newUserForm.email, company: newUserForm.company, status: newUserForm.status || 'active', password: 'user' } as ClientUser); setNewUserForm({ name: '', email: '', status: 'active', company: '' }); } }; const handleUpdateUser = () => { if (editingUser && newUserForm.name && newUserForm.email) { updateClientUser({ ...editingUser, name: newUserForm.name, email: newUserForm.email, company: newUserForm.company, status: newUserForm.status as 'active' | 'inactive' }); setEditingUser(null); setNewUserForm({ name: '', email: '', status: 'active', company: '' }); } }; const handleEditUserClick = (user: ClientUser) => { setEditingUser(user); setNewUserForm({ name: user.name, email: user.email, company: user.company, status: user.status }); }; const cancelEditUser = () => { setEditingUser(null); setNewUserForm({ name: '', email: '', status: 'active', company: '' }); }; const handleSendPasswordReset = (email: string) => { showToast(`Link di reset password inviato a ${email}`, 'success'); }; const handleAddQueue = () => { if (newQueueForm.name) { addQueue({ id: `q-${Date.now()}`, name: newQueueForm.name, description: newQueueForm.description } as TicketQueue); setNewQueueForm({ name: '', description: '' }); } }; const handleTestSmtp = () => { setIsTestingSmtp(true); setTimeout(() => { setIsTestingSmtp(false); showToast(`Test Connessione SMTP Riuscito! Host: ${tempSettings.smtp.host}`, 'success'); }, 1500); }; const handleSaveSettings = () => { setIsSaving(true); updateSettings(tempSettings); setTimeout(() => { setIsSaving(false); showToast("Impostazioni salvate con successo!", "success"); }, 800); }; const getFilteredTickets = () => { if (isViewingArchive) { return tickets.filter(t => t.status === TicketStatus.RESOLVED || t.status === TicketStatus.CLOSED); } if (selectedQueue) { return tickets.filter(t => t.queue === selectedQueue && (t.status === TicketStatus.OPEN || t.status === TicketStatus.IN_PROGRESS) ); } return []; }; const filteredTickets = getFilteredTickets(); const queueCounts: Record = {}; queues.forEach(q => { queueCounts[q.name] = tickets.filter(t => t.queue === q.name && (t.status === TicketStatus.OPEN || t.status === TicketStatus.IN_PROGRESS) ).length; }); const totalTickets = tickets.length; const resolvedCount = tickets.filter(t => t.status === TicketStatus.RESOLVED || t.status === TicketStatus.CLOSED).length; const resolutionRate = totalTickets > 0 ? Math.round((resolvedCount / totalTickets) * 100) : 0; const validSurveys = surveys || []; const ratedSurveys = validSurveys.filter(s => s.rating > 0); const avgRating = ratedSurveys.length > 0 ? (ratedSurveys.reduce((acc, curr) => acc + curr.rating, 0) / ratedSurveys.length).toFixed(1) : '0.0'; let maxQueue = 0; queues.forEach(q => { const count = tickets.filter(t => t.queue === q.name).length; if (count > maxQueue) maxQueue = count; }); return (
{/* Sidebar */}

{settings.branding.appName}

{currentUser.role === 'superadmin' ? 'Super Admin' : currentUser.role === 'supervisor' ? 'Supervisor Workspace' : 'Agent Workspace'}

{currentUser.name}

{currentUser.name}

{currentUser.queues.join(', ')}

{/* Main Content */}
{/* SETTINGS VIEW */} {view === 'settings' && canAccessSettings && (
{/* Sidebar Settings Tabs */}

Configurazione

Gestisci le preferenze globali

{/* SYSTEM SETTINGS TAB */} {settingsTab === 'system' && canManageGlobalSettings && (

Limiti e Quote di Sistema

Configura le restrizioni operative della piattaforma.

Knowledge Base
setTempSettings({...tempSettings, features: {...tempSettings.features, maxKbArticles: parseInt(e.target.value)}})} />
Team
setTempSettings({...tempSettings, features: {...tempSettings.features, maxAgents: parseInt(e.target.value)}})} />
Sicurezza
setTempSettings({...tempSettings, features: {...tempSettings.features, maxSupervisors: parseInt(e.target.value)}})} />
Automazione
setTempSettings({...tempSettings, features: {...tempSettings.features, maxAiGeneratedArticles: parseInt(e.target.value)}})} />

Interruttori Funzionalità

Permetti ai clienti di accedere agli articoli pubblici
setTempSettings({...tempSettings, features: {...tempSettings.features, kbEnabled: e.target.checked}})} />
Analisi automatica dei ticket risolti per suggerire nuovi articoli
setTempSettings({...tempSettings, features: {...tempSettings.features, aiKnowledgeAgentEnabled: e.target.checked}})} />
)} {/* GENERAL (BRANDING) TAB */} {settingsTab === 'general' && canManageGlobalSettings && (

Identità del Brand

Personalizza l'aspetto della piattaforma per i tuoi clienti.

setTempSettings({...tempSettings, branding: {...tempSettings.branding, appName: e.target.value}})} />
setTempSettings({...tempSettings, branding: {...tempSettings.branding, logoUrl: e.target.value}})} />
setTempSettings({...tempSettings, branding: {...tempSettings.branding, primaryColor: e.target.value}})} />
setTempSettings({...tempSettings, branding: {...tempSettings.branding, primaryColor: e.target.value}})} />

Anteprima Live

Questo è come appariranno i componenti principali.

)} {/* AI CONFIG TAB WITH CUSTOMIZATION */} {settingsTab === 'ai' && canManageTeam && (

Intelligenza Artificiale

Configura il cervello e la personalità del tuo assistente virtuale.

{/* Technical Config */}

Motore & Connessione

setTempSettings({...tempSettings, aiConfig: {...tempSettings.aiConfig, model: e.target.value}})} />
setTempSettings({...tempSettings, aiConfig: {...tempSettings.aiConfig, apiKey: e.target.value}})} />

La chiave viene salvata in modo sicuro nel database.

{/* Persona Config */}

Identità Chatbot

AI
Modifica URL
setTempSettings({...tempSettings, aiConfig: {...tempSettings.aiConfig, agentName: e.target.value}})} />
setTempSettings({...tempSettings, aiConfig: {...tempSettings.aiConfig, agentAvatar: e.target.value}})} />