Update ClientPortal.tsx

This commit is contained in:
fcarraUniSa
2026-02-17 14:25:25 +01:00
committed by GitHub
parent 79d173a3e4
commit df70ce6f2b

View File

@@ -21,7 +21,9 @@ import {
MoreVertical, MoreVertical,
Minus, Minus,
ExternalLink, ExternalLink,
BookOpen BookOpen,
Download,
Loader2
} from 'lucide-react'; } from 'lucide-react';
interface ClientPortalProps { interface ClientPortalProps {
@@ -30,7 +32,7 @@ interface ClientPortalProps {
queues: TicketQueue[]; queues: TicketQueue[];
settings: AppSettings; 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, attachments?: Attachment[]) => void;
onSubmitSurvey: (survey: Omit<SurveyResult, 'id' | 'timestamp'>) => void; onSubmitSurvey: (survey: Omit<SurveyResult, 'id' | 'timestamp'>) => void;
tickets?: Ticket[]; tickets?: Ticket[];
onLogout: () => void; onLogout: () => void;
@@ -77,6 +79,8 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
// Ticket Reply State // Ticket Reply State
const [replyText, setReplyText] = useState(''); const [replyText, setReplyText] = useState('');
const [replyAttachments, setReplyAttachments] = useState<Attachment[]>([]);
const [isUploading, setIsUploading] = useState(false);
// Initial Chat Message with Custom Agent Name // Initial Chat Message with Custom Agent Name
useEffect(() => { useEffect(() => {
@@ -147,7 +151,7 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
id: `att-${Date.now()}-${i}`, id: `att-${Date.now()}-${i}`,
name: file.name, name: file.name,
type: file.type, type: file.type,
url: URL.createObjectURL(file) url: URL.createObjectURL(file) // Note: This is client-side only URL for preview, real implementation would upload here too or later
}); });
} }
} }
@@ -167,11 +171,23 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
}; };
const submitReply = () => { const submitReply = () => {
if (!replyText.trim() || !selectedTicket) return; if ((!replyText.trim() && replyAttachments.length === 0) || !selectedTicket) return;
onReplyTicket(selectedTicket.id, replyText);
// Pass attachments to parent handler
onReplyTicket(selectedTicket.id, replyText, replyAttachments);
setReplyText(''); setReplyText('');
setReplyAttachments([]);
// Optimistic update for UI smoothness (actual update comes from props change) // Optimistic update for UI smoothness (actual update comes from props change)
const newMsg: ChatMessage = { id: `temp-${Date.now()}`, role: 'user', content: replyText, timestamp: new Date().toISOString() }; const newMsg: ChatMessage = {
id: `temp-${Date.now()}`,
role: 'user',
content: replyText,
attachments: replyAttachments,
timestamp: new Date().toISOString()
};
setSelectedTicket({ setSelectedTicket({
...selectedTicket, ...selectedTicket,
messages: [...selectedTicket.messages, newMsg], messages: [...selectedTicket.messages, newMsg],
@@ -180,6 +196,51 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
showToast("Risposta inviata", 'success'); showToast("Risposta inviata", 'success');
}; };
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
// Validation
const maxSize = (settings.features.maxFileSizeMb || 5) * 1024 * 1024;
if (file.size > maxSize) {
showToast(`File troppo grande. Max ${settings.features.maxFileSizeMb}MB`, 'error');
return;
}
const allowed = (settings.features.allowedFileTypes || 'jpg,png,pdf').split(',').map(t => t.trim().toLowerCase());
const ext = file.name.split('.').pop()?.toLowerCase() || '';
if (!allowed.includes(ext)) {
showToast(`Estensione non permessa. Ammessi: ${settings.features.allowedFileTypes}`, 'error');
return;
}
setIsUploading(true);
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.url) {
setReplyAttachments(prev => [...prev, { id: data.id, name: data.name, url: data.url, type: data.type }]);
}
} catch (err) {
showToast('Errore caricamento file', 'error');
} finally {
setIsUploading(false);
// Reset input
e.target.value = '';
}
}
};
const removeAttachment = (id: string) => {
setReplyAttachments(prev => prev.filter(a => a.id !== id));
};
const submitSurvey = () => { const submitSurvey = () => {
if (surveyData.rating === 0) { if (surveyData.rating === 0) {
showToast("Per favore seleziona una valutazione.", 'error'); showToast("Per favore seleziona una valutazione.", 'error');
@@ -334,12 +395,39 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
{selectedTicket.messages.map(m => ( {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'}`}> <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> <p className="text-sm">{m.content}</p>
{m.attachments && m.attachments.length > 0 && (
<div className={`mt-2 pt-2 border-t ${m.role === 'user' ? 'border-blue-500/30' : 'border-gray-200/50'}`}>
{m.attachments.map(att => (
<a key={att.id} href={att.url} target="_blank" rel="noopener noreferrer" className={`flex items-center text-xs hover:underline mt-1 ${m.role === 'user' ? 'text-white' : 'text-blue-600'}`}>
<Download className="w-3 h-3 mr-1" /> {att.name}
</a>
))}
</div>
)}
</div>
))}
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-wrap gap-2">
{replyAttachments.map(att => (
<div key={att.id} className="flex items-center text-xs bg-gray-100 px-2 py-1 rounded-full">
<span className="max-w-[100px] truncate">{att.name}</span>
<button onClick={() => removeAttachment(att.id)} className="ml-1 text-gray-500 hover:text-red-500"><X className="w-3 h-3"/></button>
</div> </div>
))} ))}
</div> </div>
<div className="flex gap-2">
<textarea className="flex-1 border p-2 rounded" value={replyText} onChange={e => setReplyText(e.target.value)} placeholder="Scrivi risposta..." /> <textarea className="flex-1 border p-2 rounded" value={replyText} onChange={e => setReplyText(e.target.value)} placeholder="Scrivi risposta..." />
<button onClick={submitReply} className="bg-brand-600 text-white px-4 rounded font-bold">Invia</button> <div className="flex justify-between items-center">
<div>
{settings.features.attachmentsEnabled !== false && (
<label className={`cursor-pointer inline-flex items-center p-2 rounded-lg text-gray-500 hover:bg-gray-100 transition ${isUploading ? 'opacity-50 pointer-events-none' : ''}`}>
{isUploading ? <Loader2 className="w-5 h-5 animate-spin" /> : <Paperclip className="w-5 h-5" />}
<input type="file" className="hidden" onChange={handleFileUpload} />
</label>
)}
</div>
<button onClick={submitReply} disabled={isUploading} className="bg-brand-600 text-white px-4 py-2 rounded font-bold disabled:opacity-50">Invia</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -355,6 +443,8 @@ export const ClientPortal: React.FC<ClientPortalProps> = ({
<select className="w-full border p-3 rounded-lg" value={ticketForm.queue} onChange={e => setTicketForm({...ticketForm, queue: e.target.value})}> <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>)} {queues.map(q => <option key={q.id} value={q.name}>{q.name}</option>)}
</select> </select>
{/* Create Ticket attachment (mock for now as backend requires form data or separate upload) */}
{/* For simplicity in this demo, omitting file upload on creation to keep code lean, or would need same async upload logic */}
<button type="submit" className="w-full bg-brand-600 text-white py-3 rounded-lg font-bold">Crea Ticket</button> <button type="submit" className="w-full bg-brand-600 text-white py-3 rounded-lg font-bold">Crea Ticket</button>
</form> </form>
</div> </div>