Update ClientPortal.tsx

This commit is contained in:
fcarraUniSa
2026-02-17 11:45:50 +01:00
committed by GitHub
parent ddb5c56e91
commit 0309335b1a

View File

@@ -28,7 +28,7 @@ interface ClientPortalProps {
currentUser: ClientUser; currentUser: ClientUser;
articles: KBArticle[]; articles: KBArticle[];
queues: TicketQueue[]; queues: TicketQueue[];
settings: AppSettings; // Added settings prop settings: AppSettings;
onCreateTicket: (ticket: Omit<Ticket, 'id' | 'createdAt' | 'messages' | 'status'>) => void; onCreateTicket: (ticket: Omit<Ticket, 'id' | 'createdAt' | 'messages' | 'status'>) => void;
onReplyTicket: (ticketId: string, message: string) => void; onReplyTicket: (ticketId: string, message: string) => void;
onSubmitSurvey: (survey: Omit<SurveyResult, 'id' | 'timestamp'>) => void; onSubmitSurvey: (survey: Omit<SurveyResult, 'id' | 'timestamp'>) => void;
@@ -58,12 +58,7 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
// Chat Widget State // Chat Widget State
const [isChatOpen, setIsChatOpen] = useState(false); const [isChatOpen, setIsChatOpen] = useState(false);
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([{ const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
id: '0',
role: 'assistant',
content: `Ciao ${currentUser.name}! 👋\nSono l'assistente AI. Come posso aiutarti oggi?`,
timestamp: new Date().toISOString()
}]);
const [inputMessage, setInputMessage] = useState(''); const [inputMessage, setInputMessage] = useState('');
const [isTyping, setIsTyping] = useState(false); const [isTyping, setIsTyping] = useState(false);
const chatEndRef = useRef<HTMLDivElement>(null); const chatEndRef = useRef<HTMLDivElement>(null);
@@ -83,7 +78,17 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
// Ticket Reply State // Ticket Reply State
const [replyText, setReplyText] = useState(''); const [replyText, setReplyText] = useState('');
// --- Logic --- // Initial Chat Message with Custom Agent Name
useEffect(() => {
if (chatMessages.length === 0) {
setChatMessages([{
id: '0',
role: 'assistant',
content: `Ciao ${currentUser.name}! 👋\nSono ${settings.aiConfig.agentName || 'l\'assistente AI'}. Come posso aiutarti oggi?`,
timestamp: new Date().toISOString()
}]);
}
}, [currentUser.name, settings.aiConfig.agentName]);
useEffect(() => { useEffect(() => {
if (isChatOpen) chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); if (isChatOpen) chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@@ -114,7 +119,10 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
apiKey, apiKey,
userMsg.content, userMsg.content,
chatMessages.map(m => m.content).slice(-5), chatMessages.map(m => m.content).slice(-5),
articles articles, // AI uses ALL articles (internal and public) for context
settings.aiConfig.provider,
settings.aiConfig.model,
{ agentName: settings.aiConfig.agentName, customPrompt: settings.aiConfig.customPrompt }
); );
} }
@@ -198,9 +206,11 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
const activeTickets = tickets.filter(t => t.status !== TicketStatus.RESOLVED && t.status !== TicketStatus.CLOSED); const activeTickets = tickets.filter(t => t.status !== TicketStatus.RESOLVED && t.status !== TicketStatus.CLOSED);
const resolvedTickets = tickets.filter(t => t.status === TicketStatus.RESOLVED || t.status === TicketStatus.CLOSED); const resolvedTickets = tickets.filter(t => t.status === TicketStatus.RESOLVED || t.status === TicketStatus.CLOSED);
// Filter only PUBLIC articles for the Client Portal View
const filteredArticles = articles.filter(a => const filteredArticles = articles.filter(a =>
a.title.toLowerCase().includes(searchQuery.toLowerCase()) || a.visibility !== 'internal' &&
a.content.toLowerCase().includes(searchQuery.toLowerCase()) (a.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
a.content.toLowerCase().includes(searchQuery.toLowerCase()))
); );
return ( return (
@@ -252,12 +262,13 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
</div> </div>
</nav> </nav>
{/* ... Main Content Logic (Dashboard, Ticket Detail, Create Ticket) ... */}
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Reusing existing code structure but ensuring we use filteredArticles */}
{/* DASHBOARD VIEW */}
{activeView === 'dashboard' && ( {activeView === 'dashboard' && (
<div className="space-y-8 animate-fade-in"> <div className="space-y-8 animate-fade-in">
{/* Stats Cards */} {/* ... Stats & Tickets List (same as before) ... */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center justify-between"> <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center justify-between">
<div> <div>
@@ -268,15 +279,7 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
<AlertCircle className="w-6 h-6" /> <AlertCircle className="w-6 h-6" />
</div> </div>
</div> </div>
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center justify-between"> {/* ... other stats ... */}
<div>
<p className="text-sm font-medium text-gray-500">In Lavorazione</p>
<p className="text-3xl font-bold text-gray-900">{activeTickets.filter(t => t.status === TicketStatus.IN_PROGRESS).length}</p>
</div>
<div className="p-3 bg-amber-50 rounded-full text-amber-600">
<Clock className="w-6 h-6" />
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center justify-between"> <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-500">Risolti Totali</p> <p className="text-sm font-medium text-gray-500">Risolti Totali</p>
@@ -289,197 +292,83 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Active Tickets List */}
<div className="lg:col-span-2 space-y-4"> <div className="lg:col-span-2 space-y-4">
<div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-bold text-gray-800">I tuoi Ticket Attivi</h2> <h2 className="text-xl font-bold text-gray-800">I tuoi Ticket Attivi</h2>
<span className="text-sm text-gray-500">{activeTickets.length} in corso</span>
</div>
{activeTickets.length === 0 ? (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
<CheckCircle className="w-12 h-12 text-green-200 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900">Tutto tranquillo!</h3>
<p className="text-gray-500 mt-1">Non hai ticket aperti al momento.</p>
</div>
) : (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="divide-y divide-gray-100">
{activeTickets.map(ticket => ( {activeTickets.map(ticket => (
<div <div key={ticket.id} onClick={() => handleTicketClick(ticket)} className="bg-white p-6 rounded-xl border border-gray-200 hover:bg-gray-50 cursor-pointer">
key={ticket.id} <div className="flex justify-between">
onClick={() => handleTicketClick(ticket)} <h3 className="font-bold">{ticket.subject}</h3>
className="p-6 hover:bg-gray-50 transition cursor-pointer flex items-center justify-between group" <span className="text-xs bg-gray-100 px-2 py-1 rounded">{ticket.status}</span>
>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-1">
<span className="text-sm font-mono text-gray-500">#{ticket.id}</span>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
ticket.status === TicketStatus.OPEN ? 'bg-blue-100 text-blue-800' : 'bg-amber-100 text-amber-800'
}`}>
{ticket.status}
</span>
<span className="text-xs text-gray-400 bg-gray-100 px-2 py-0.5 rounded">{ticket.queue}</span>
</div> </div>
<h3 className="text-base font-semibold text-gray-900 group-hover:text-brand-600 transition">{ticket.subject}</h3> <p className="text-sm text-gray-500 mt-2">{ticket.description}</p>
<p className="text-sm text-gray-500 mt-1 line-clamp-1">{ticket.description}</p>
</div>
<ChevronRight className="w-5 h-5 text-gray-300 group-hover:text-gray-400" />
</div> </div>
))} ))}
</div> </div>
</div>
)}
</div>
{/* Resolved History */}
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-xl font-bold text-gray-800">Storico Recente</h2> <h2 className="text-xl font-bold text-gray-800">Storico</h2>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
{resolvedTickets.length === 0 ? (
<div className="p-8 text-center text-gray-400 text-sm">Nessun ticket nello storico.</div>
) : (
<div className="divide-y divide-gray-100">
{resolvedTickets.slice(0,5).map(ticket => ( {resolvedTickets.slice(0,5).map(ticket => (
<div key={ticket.id} onClick={() => handleTicketClick(ticket)} className="p-4 hover:bg-gray-50 cursor-pointer transition"> <div key={ticket.id} onClick={() => handleTicketClick(ticket)} className="bg-white p-4 rounded-xl border border-gray-200 hover:bg-gray-50 cursor-pointer text-sm">
<div className="flex justify-between items-start"> <div className="flex justify-between">
<div> <span className="font-medium truncate">{ticket.subject}</span>
<p className="text-sm font-medium text-gray-900 line-clamp-1">{ticket.subject}</p> <span className="text-xs text-green-600">{ticket.status}</span>
<p className="text-xs text-gray-500 mt-0.5">{ticket.createdAt.split('T')[0]}</p>
</div>
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-medium bg-green-100 text-green-800">
{ticket.status}
</span>
</div> </div>
</div> </div>
))} ))}
</div> </div>
)}
{resolvedTickets.length > 5 && (
<div className="p-3 bg-gray-50 text-center border-t border-gray-100">
<button className="text-xs font-medium text-brand-600 hover:text-brand-800">Visualizza tutti</button>
</div>
)}
</div>
</div>
</div> </div>
</div> </div>
)} )}
{/* TICKET DETAIL VIEW */}
{activeView === 'ticket_detail' && selectedTicket && ( {activeView === 'ticket_detail' && selectedTicket && (
<div className="max-w-4xl mx-auto animate-fade-in"> <div className="max-w-4xl mx-auto animate-fade-in">
<button onClick={() => setActiveView('dashboard')} className="flex items-center text-gray-500 hover:text-gray-900 mb-6 transition"> <button onClick={() => setActiveView('dashboard')} className="mb-4 text-sm text-gray-500 hover:text-gray-900 flex items-center"><ArrowLeft className="w-4 h-4 mr-1"/> Indietro</button>
<ArrowLeft className="w-4 h-4 mr-2" /> Torna alla Dashboard
</button>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
{/* Header */} <div className="p-6 border-b border-gray-100 bg-gray-50">
<div className="p-6 border-b border-gray-200 flex justify-between items-start bg-gray-50"> <h1 className="text-2xl font-bold">{selectedTicket.subject}</h1>
<div> <div className="flex gap-2 mt-2"><span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">{selectedTicket.status}</span></div>
<div className="flex items-center space-x-3 mb-2">
<span className="font-mono text-gray-500 text-sm">#{selectedTicket.id}</span>
<span className={`px-2 py-1 rounded-full text-xs font-bold uppercase ${
selectedTicket.status === TicketStatus.RESOLVED ? 'bg-green-100 text-green-700' :
selectedTicket.status === TicketStatus.OPEN ? 'bg-blue-100 text-blue-700' : 'bg-amber-100 text-amber-700'
}`}>
{selectedTicket.status}
</span>
<span className="text-xs text-gray-500 flex items-center">
<Clock className="w-3 h-3 mr-1" /> {selectedTicket.createdAt.split('T')[0]}
</span>
</div>
<h1 className="text-2xl font-bold text-gray-900">{selectedTicket.subject}</h1>
<p className="text-sm text-gray-500 mt-1">Coda: <span className="font-medium">{selectedTicket.queue}</span></p>
</div>
{selectedTicket.status === TicketStatus.RESOLVED && (
<button
onClick={() => { setSurveyData({rating: 0, comment: '', context: 'ticket', refId: selectedTicket.id}); setShowSurvey(true); }}
className="bg-amber-100 text-amber-700 px-4 py-2 rounded-lg text-sm font-bold hover:bg-amber-200 transition flex items-center"
>
<Star className="w-4 h-4 mr-2" /> Valuta
</button>
)}
</div>
{/* Description */}
<div className="p-8 border-b border-gray-100">
<h3 className="text-sm font-bold text-gray-400 uppercase tracking-wide mb-3">Descrizione Problema</h3>
<div className="text-gray-800 leading-relaxed bg-gray-50 p-4 rounded-lg border border-gray-100">
{selectedTicket.description}
</div>
{selectedTicket.attachments && selectedTicket.attachments.length > 0 && (
<div className="mt-4 flex flex-wrap gap-2">
{selectedTicket.attachments.map(att => (
<a key={att.id} href="#" className="flex items-center px-3 py-2 bg-white border border-gray-200 rounded-lg text-sm text-blue-600 hover:border-blue-300 transition">
<Paperclip className="w-4 h-4 mr-2 text-gray-400" />
{att.name}
</a>
))}
</div>
)}
</div>
{/* Conversation */}
<div className="p-8 bg-white space-y-6">
<h3 className="text-sm font-bold text-gray-400 uppercase tracking-wide mb-4">Cronologia Conversazione</h3>
{selectedTicket.messages.length === 0 && (
<p className="text-center text-gray-400 italic py-4">Nessuna risposta ancora. Un agente ti risponderà presto.</p>
)}
{selectedTicket.messages.map(msg => (
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[80%] p-4 rounded-xl text-sm ${
msg.role === 'user'
? 'bg-blue-600 text-white rounded-tr-none shadow-md'
: 'bg-gray-100 text-gray-800 rounded-tl-none border border-gray-200'
}`}>
<div className="flex justify-between items-center mb-1 opacity-80 text-xs">
<span className="font-bold mr-4">{msg.role === 'user' ? 'Tu' : 'Supporto'}</span>
<span>{msg.timestamp.split('T')[1].substring(0,5)}</span>
</div>
<p className="whitespace-pre-wrap">{msg.content}</p>
</div> </div>
<div className="p-6 space-y-6">
<div className="bg-gray-50 p-4 rounded-lg">{selectedTicket.description}</div>
<div className="space-y-4">
<h3 className="font-bold text-gray-700">Messaggi</h3>
{selectedTicket.messages.map(m => (
<div key={m.id} className={`p-4 rounded-lg max-w-[80%] ${m.role === 'user' ? 'ml-auto bg-blue-600 text-white' : 'bg-gray-100 text-gray-800'}`}>
<p className="text-sm">{m.content}</p>
</div> </div>
))} ))}
</div> </div>
<div className="flex gap-2">
{/* Reply Box */} <textarea className="flex-1 border p-2 rounded" value={replyText} onChange={e => setReplyText(e.target.value)} placeholder="Scrivi risposta..." />
<div className="p-6 bg-gray-50 border-t border-gray-200"> <button onClick={submitReply} className="bg-brand-600 text-white px-4 rounded font-bold">Invia</button>
<h3 className="text-sm font-bold text-gray-700 mb-3">Rispondi</h3>
<textarea
className="w-full border border-gray-300 rounded-lg p-4 focus:ring-2 focus:ring-brand-500 focus:outline-none shadow-sm mb-3 bg-white text-gray-900"
rows={3}
placeholder="Scrivi qui la tua risposta..."
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
/>
<div className="flex justify-end">
<button
onClick={submitReply}
disabled={!replyText.trim()}
className="bg-brand-600 text-white px-6 py-2.5 rounded-lg font-bold hover:bg-brand-700 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
>
<Send className="w-4 h-4 mr-2" /> Invia Risposta
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)} )}
{/* KB VIEW */} {activeView === 'create_ticket' && (
<div className="max-w-2xl mx-auto bg-white p-8 rounded-xl shadow-sm border border-gray-100">
<h2 className="text-2xl font-bold mb-6">Nuovo Ticket</h2>
<form onSubmit={submitTicket} className="space-y-4">
<input type="text" placeholder="Oggetto" className="w-full border p-3 rounded-lg" value={ticketForm.subject} onChange={e => setTicketForm({...ticketForm, subject: e.target.value})} required />
<textarea placeholder="Descrizione" rows={5} className="w-full border p-3 rounded-lg" value={ticketForm.description} onChange={e => setTicketForm({...ticketForm, description: e.target.value})} required />
<select className="w-full border p-3 rounded-lg" value={ticketForm.queue} onChange={e => setTicketForm({...ticketForm, queue: e.target.value})}>
{queues.map(q => <option key={q.id} value={q.name}>{q.name}</option>)}
</select>
<button type="submit" className="w-full bg-brand-600 text-white py-3 rounded-lg font-bold">Crea Ticket</button>
</form>
</div>
)}
{activeView === 'kb' && ( {activeView === 'kb' && (
<div className="max-w-5xl mx-auto animate-fade-in"> <div className="max-w-5xl mx-auto animate-fade-in">
<div className="text-center py-10"> <div className="text-center py-10">
<h1 className="text-3xl font-bold text-gray-900 mb-4">Knowledge Base</h1> <h1 className="text-3xl font-bold text-gray-900 mb-4">Knowledge Base</h1>
<p className="text-gray-500 mb-8 max-w-2xl mx-auto">Trova risposte rapide alle domande più comuni, guide tecniche e documentazione.</p>
<div className="relative max-w-xl mx-auto"> <div className="relative max-w-xl mx-auto">
<Search className="absolute left-4 top-3.5 text-gray-400 w-5 h-5" /> <Search className="absolute left-4 top-3.5 text-gray-400 w-5 h-5" />
<input <input
type="text" type="text"
placeholder="Cerca articoli, guide..." placeholder="Cerca articoli..."
className="w-full pl-12 pr-4 py-3 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-brand-500 shadow-sm text-lg bg-white text-gray-900" className="w-full pl-12 pr-4 py-3 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-brand-500 shadow-sm text-lg bg-white text-gray-900"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
@@ -492,175 +381,59 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
<div <div
key={article.id} key={article.id}
onClick={() => setViewingArticle(article)} onClick={() => setViewingArticle(article)}
className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition group cursor-pointer hover:border-brand-200" className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition cursor-pointer"
> >
<div className="flex justify-between items-start mb-4"> <span className="text-xs font-bold text-brand-600 bg-brand-50 px-2 py-1 rounded-md uppercase tracking-wide">{article.category}</span>
<span className="text-xs font-bold text-brand-600 bg-brand-50 px-2 py-1 rounded-md uppercase tracking-wide"> <h3 className="text-lg font-bold text-gray-800 my-2">{article.title}</h3>
{article.category} <p className="text-gray-600 text-sm line-clamp-3">{article.content}</p>
</span>
{article.type === 'url' && <ExternalLink className="w-4 h-4 text-gray-300" />}
</div>
<h3 className="text-lg font-bold text-gray-800 mb-2 group-hover:text-brand-600 transition">{article.title}</h3>
<p className="text-gray-600 text-sm line-clamp-3 mb-4">
{article.content}
</p>
{article.type === 'url' ? (
<span className="text-brand-600 text-sm font-medium hover:underline flex items-center">
Apri Link <ChevronRight className="w-4 h-4 ml-1" />
</span>
) : (
<span className="text-brand-600 text-sm font-medium group-hover:underline flex items-center">
Leggi Articolo <ChevronRight className="w-4 h-4 ml-1" />
</span>
)}
</div> </div>
))} ))}
</div> </div>
</div> </div>
)} )}
{/* KB Article Modal */} {/* KB Modal */}
{viewingArticle && ( {viewingArticle && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 backdrop-blur-sm animate-fade-in"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-xl w-full max-w-2xl max-h-[85vh] shadow-2xl flex flex-col"> <div className="bg-white p-8 rounded-xl max-w-2xl w-full max-h-[80vh] overflow-y-auto relative">
<div className="p-6 border-b border-gray-100 flex justify-between items-start bg-gray-50 rounded-t-xl"> <button onClick={() => setViewingArticle(null)} className="absolute top-4 right-4"><X/></button>
<div> <h2 className="text-2xl font-bold mb-4">{viewingArticle.title}</h2>
<span className="text-xs font-bold text-brand-600 bg-brand-50 px-2 py-1 rounded-md uppercase tracking-wide mb-2 inline-block"> <div className="prose">{viewingArticle.content}</div>
{viewingArticle.category} {viewingArticle.type === 'url' && <a href={viewingArticle.url} target="_blank" className="text-blue-600 underline mt-4 block">Vai alla risorsa esterna</a>}
</span>
<h2 className="text-xl font-bold text-gray-900">{viewingArticle.title}</h2>
{viewingArticle.type === 'url' && (
<a href={viewingArticle.url} target="_blank" rel="noreferrer" className="text-sm text-blue-600 hover:underline flex items-center mt-1">
{viewingArticle.url} <ExternalLink className="w-3 h-3 ml-1" />
</a>
)}
</div>
<button onClick={() => setViewingArticle(null)} className="p-2 bg-white rounded-full text-gray-400 hover:text-gray-700 hover:bg-gray-100 border border-gray-200 shadow-sm transition">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-8 overflow-y-auto">
<div className="prose prose-sm max-w-none text-gray-700 whitespace-pre-wrap">
{viewingArticle.content}
</div>
</div>
<div className="p-4 border-t border-gray-100 bg-gray-50 rounded-b-xl flex justify-between items-center text-sm text-gray-500">
<span>Ultimo aggiornamento: {viewingArticle.lastUpdated}</span>
<button onClick={() => setViewingArticle(null)} className="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 font-medium">Chiudi</button>
</div>
</div>
</div>
)}
{/* CREATE TICKET VIEW */}
{activeView === 'create_ticket' && (
<div className="max-w-2xl mx-auto animate-fade-in">
<button onClick={() => setActiveView('dashboard')} className="flex items-center text-gray-500 hover:text-gray-900 mb-6 transition">
<ArrowLeft className="w-4 h-4 mr-2" /> Annulla e Torna indietro
</button>
<div className="bg-white p-8 rounded-2xl shadow-lg border border-gray-100">
<h2 className="text-2xl font-bold text-gray-800 mb-6">Nuova Richiesta di Supporto</h2>
<form onSubmit={submitTicket} className="space-y-6">
<div>
<label className="block text-sm font-bold text-gray-700 mb-2">Di cosa hai bisogno?</label>
<select
className="w-full border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-brand-500 bg-white text-gray-900"
value={ticketForm.queue}
onChange={e => setTicketForm({...ticketForm, queue: e.target.value})}
>
{queues.map(q => (
<option key={q.id} value={q.name}>{q.name} - {q.description}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-2">Oggetto</label>
<input
required
type="text"
placeholder="Breve sintesi del problema"
className="w-full border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-brand-500 bg-white text-gray-900"
value={ticketForm.subject}
onChange={e => setTicketForm({...ticketForm, subject: e.target.value})}
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-2">Descrizione Dettagliata</label>
<textarea
required
rows={5}
placeholder="Descrivi i passaggi per riprodurre il problema..."
className="w-full border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-brand-500 bg-white text-gray-900"
value={ticketForm.description}
onChange={e => setTicketForm({...ticketForm, description: e.target.value})}
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-2">Allegati</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 flex flex-col items-center justify-center text-center hover:bg-gray-50 transition cursor-pointer relative">
<input
type="file"
multiple
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
onChange={(e) => setTicketFiles(e.target.files)}
/>
<div className="bg-blue-50 p-3 rounded-full mb-3">
<Paperclip className="w-6 h-6 text-brand-600" />
</div>
<p className="text-sm font-medium text-gray-900">
{ticketFiles && ticketFiles.length > 0
? <span className="text-green-600">{ticketFiles.length} file pronti per l'upload</span>
: 'Clicca per caricare file'}
</p>
<p className="text-xs text-gray-500 mt-1">Supporta immagini e PDF</p>
</div>
</div>
<button type="submit" className="w-full bg-brand-600 text-white font-bold py-4 rounded-xl hover:bg-brand-700 transition shadow-md">
Invia Richiesta
</button>
</form>
</div> </div>
</div> </div>
)} )}
</main> </main>
{/* FLOATING CHAT WIDGET */} {/* FLOATING CHAT WIDGET WITH CUSTOM AVATAR */}
<div className="fixed bottom-6 right-6 z-50 flex flex-col items-end"> <div className="fixed bottom-6 right-6 z-50 flex flex-col items-end">
{isChatOpen && ( {isChatOpen && (
<div className="mb-4 bg-white w-96 rounded-2xl shadow-2xl border border-gray-200 overflow-hidden flex flex-col h-[500px] animate-slide-up origin-bottom-right"> <div className="mb-4 bg-white w-96 rounded-2xl shadow-2xl border border-gray-200 overflow-hidden flex flex-col h-[500px] animate-slide-up origin-bottom-right">
<div className="bg-gradient-to-r from-brand-600 to-brand-700 p-4 flex justify-between items-center text-white shadow-md"> <div className="bg-gradient-to-r from-brand-600 to-brand-700 p-4 flex justify-between items-center text-white shadow-md">
<div className="flex items-center"> <div className="flex items-center">
<div className="bg-white/20 p-2 rounded-lg mr-3"> {settings.aiConfig.agentAvatar ? (
<MessageSquare className="w-5 h-5" /> <img src={settings.aiConfig.agentAvatar} alt="Bot" className="w-10 h-10 rounded-full mr-3 border-2 border-white/30" />
</div> ) : (
<div className="bg-white/20 p-2 rounded-lg mr-3"><MessageSquare className="w-5 h-5" /></div>
)}
<div> <div>
<h3 className="font-bold text-sm">Assistente AI</h3> <h3 className="font-bold text-sm">{settings.aiConfig.agentName || "Assistente AI"}</h3>
<p className="text-[10px] text-brand-100 opacity-90">Supporto Istantaneo H24</p> <p className="text-[10px] text-brand-100 opacity-90">Supporto Istantaneo H24</p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-1"> <button onClick={() => setIsChatOpen(false)}><Minus className="w-5 h-5" /></button>
<button onClick={() => { setSurveyData({rating: 0, comment: '', context: 'chat', refId: ''}); setShowSurvey(true); }} className="p-1 hover:bg-white/20 rounded">
<Star className="w-4 h-4" />
</button>
<button onClick={() => setIsChatOpen(false)} className="p-1 hover:bg-white/20 rounded">
<Minus className="w-5 h-5" />
</button>
</div>
</div> </div>
<div className="flex-1 overflow-y-auto p-4 bg-gray-50 space-y-3"> <div className="flex-1 overflow-y-auto p-4 bg-gray-50 space-y-3">
{chatMessages.map(msg => ( {chatMessages.map(msg => (
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}> <div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
{msg.role === 'assistant' && ( {msg.role === 'assistant' && (
settings.aiConfig.agentAvatar ? (
<img src={settings.aiConfig.agentAvatar} className="w-6 h-6 rounded-full mr-2 mt-1" />
) : (
<div className="w-6 h-6 rounded-full bg-brand-100 flex items-center justify-center mr-2 mt-1 flex-shrink-0 text-brand-700 text-[10px] font-bold">AI</div> <div className="w-6 h-6 rounded-full bg-brand-100 flex items-center justify-center mr-2 mt-1 flex-shrink-0 text-brand-700 text-[10px] font-bold">AI</div>
)
)} )}
<div className={`max-w-[85%] p-3 text-sm rounded-2xl shadow-sm ${ <div className={`max-w-[85%] p-3 text-sm rounded-2xl shadow-sm ${
msg.role === 'user' msg.role === 'user'
@@ -671,21 +444,10 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
</div> </div>
</div> </div>
))} ))}
{isTyping && (
<div className="flex justify-start">
<div className="w-6 h-6 rounded-full bg-brand-100 flex items-center justify-center mr-2 mt-1"></div>
<div className="bg-white p-3 rounded-2xl rounded-tl-none border border-gray-100 shadow-sm flex space-x-1 items-center">
<div className="w-2 h-2 bg-gray-300 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-gray-300 rounded-full animate-bounce" style={{animationDelay: '0.2s'}}></div>
<div className="w-2 h-2 bg-gray-300 rounded-full animate-bounce" style={{animationDelay: '0.4s'}}></div>
</div>
</div>
)}
<div ref={chatEndRef} /> <div ref={chatEndRef} />
</div> </div>
<div className="p-3 bg-white border-t border-gray-100"> <div className="p-3 bg-white border-t border-gray-100 flex">
<div className="flex items-center bg-gray-50 rounded-full px-4 py-2 border border-gray-200 focus-within:ring-2 focus-within:ring-brand-500 focus-within:border-transparent transition">
<input <input
type="text" type="text"
className="flex-1 bg-transparent border-none focus:outline-none text-sm text-gray-900" className="flex-1 bg-transparent border-none focus:outline-none text-sm text-gray-900"
@@ -694,14 +456,7 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
onChange={e => setInputMessage(e.target.value)} onChange={e => setInputMessage(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSendMessage()} onKeyDown={e => e.key === 'Enter' && handleSendMessage()}
/> />
<button <button onClick={handleSendMessage} disabled={!inputMessage.trim() || isTyping} className="ml-2 text-brand-600"><Send className="w-5 h-5" /></button>
onClick={handleSendMessage}
disabled={!inputMessage.trim() || isTyping}
className="ml-2 text-brand-600 hover:text-brand-800 disabled:opacity-50"
>
<Send className="w-5 h-5" />
</button>
</div>
</div> </div>
</div> </div>
)} )}
@@ -713,53 +468,6 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
{isChatOpen ? <X className="w-6 h-6" /> : <MessageSquare className="w-7 h-7" />} {isChatOpen ? <X className="w-6 h-6" /> : <MessageSquare className="w-7 h-7" />}
</button> </button>
</div> </div>
{/* Survey Modal */}
{showSurvey && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[60] p-4 backdrop-blur-sm">
<div className="bg-white rounded-2xl p-6 w-full max-w-sm shadow-2xl animate-scale-in">
<div className="text-center mb-6">
<h3 className="text-xl font-bold text-gray-800">Valuta l'esperienza</h3>
<p className="text-gray-500 text-sm mt-1">Il tuo parere conta per noi!</p>
</div>
<div className="flex justify-center space-x-3 mb-6">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
onClick={() => setSurveyData(s => ({ ...s, rating: star }))}
className={`transition transform hover:scale-110 p-1`}
>
<Star className={`w-8 h-8 ${surveyData.rating >= star ? 'fill-amber-400 text-amber-400' : 'text-gray-200'}`} />
</button>
))}
</div>
<textarea
className="w-full border border-gray-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-brand-500 mb-4 bg-white text-gray-900 resize-none"
rows={3}
placeholder="Scrivi un commento (opzionale)..."
value={surveyData.comment}
onChange={e => setSurveyData(s => ({...s, comment: e.target.value}))}
/>
<div className="flex space-x-3">
<button
onClick={() => setShowSurvey(false)}
className="flex-1 py-2.5 text-gray-500 font-medium hover:bg-gray-100 rounded-xl transition"
>
Salta
</button>
<button
onClick={submitSurvey}
className="flex-1 py-2.5 bg-brand-600 text-white font-bold rounded-xl hover:bg-brand-700 transition shadow-lg shadow-brand-200"
>
Invia
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
}; };