433 lines
15 KiB
TypeScript
433 lines
15 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { AgentDashboard } from './components/AgentDashboard';
|
|
import { ClientPortal } from './components/ClientPortal';
|
|
import { AuthScreen } from './components/AuthScreen';
|
|
import { ToastContainer, ToastMessage, ToastType } from './components/Toast';
|
|
import { INITIAL_SETTINGS } from './constants';
|
|
import { Agent, AppSettings, AppState, ClientUser, KBArticle, Ticket, TicketStatus, SurveyResult, TicketQueue, ChatMessage, AgentRole, Attachment } from './types';
|
|
|
|
const App: React.FC = () => {
|
|
const [state, setState] = useState<AppState>({
|
|
tickets: [],
|
|
articles: [],
|
|
agents: [],
|
|
queues: [],
|
|
surveys: [],
|
|
clientUsers: [],
|
|
settings: INITIAL_SETTINGS,
|
|
currentUser: null,
|
|
userRole: 'guest'
|
|
});
|
|
|
|
const [toasts, setToasts] = useState<ToastMessage[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// --- API HELPER ---
|
|
const apiFetch = async (endpoint: string, options: RequestInit = {}) => {
|
|
try {
|
|
const res = await fetch(`/api${endpoint}`, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
...options
|
|
});
|
|
if (!res.ok) throw new Error(`API Error: ${res.statusText}`);
|
|
return await res.json();
|
|
} catch (error) {
|
|
console.error(error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
// Load initial config data
|
|
const data = await apiFetch('/initial-data');
|
|
|
|
// Load tickets
|
|
const tickets = await apiFetch('/tickets');
|
|
|
|
setState(prev => ({
|
|
...prev,
|
|
agents: data.agents,
|
|
clientUsers: data.clientUsers,
|
|
queues: data.queues,
|
|
articles: data.articles,
|
|
surveys: data.surveys,
|
|
tickets: tickets,
|
|
settings: data.settings || prev.settings
|
|
}));
|
|
} catch (e) {
|
|
showToast("Errore caricamento dati dal server", 'error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
// Carica dati al caricamento se l'utente è loggato
|
|
if (state.currentUser) {
|
|
loadData();
|
|
}
|
|
}, [state.currentUser]);
|
|
|
|
// Effect to refresh tickets periodically if logged in
|
|
useEffect(() => {
|
|
if (state.currentUser) {
|
|
const interval = setInterval(loadData, 10000); // Poll every 10s
|
|
return () => clearInterval(interval);
|
|
}
|
|
}, [state.currentUser]);
|
|
|
|
|
|
const showToast = (message: string, type: ToastType = 'info') => {
|
|
const id = Date.now().toString();
|
|
setToasts(prev => [...prev, { id, message, type }]);
|
|
};
|
|
|
|
const removeToast = (id: string) => {
|
|
setToasts(prev => prev.filter(t => t.id !== id));
|
|
};
|
|
|
|
// --- Auth Management ---
|
|
const handleLogin = async (email: string, pass: string, isAgent: boolean) => {
|
|
try {
|
|
const res = await apiFetch('/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email, password: pass })
|
|
});
|
|
|
|
if (res.user) {
|
|
setState(prev => ({ ...prev, currentUser: res.user, userRole: res.role }));
|
|
showToast(`Benvenuto ${res.user.name}`, 'success');
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const handleClientRegister = async (name: string, email: string, pass: string, company: string) => {
|
|
try {
|
|
const res = await apiFetch('/auth/register', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ name, email, password: pass, company })
|
|
});
|
|
|
|
if (res.success) {
|
|
setState(prev => ({
|
|
...prev,
|
|
clientUsers: [...prev.clientUsers, res.user],
|
|
currentUser: res.user,
|
|
userRole: 'client'
|
|
}));
|
|
showToast("Registrazione completata!", 'success');
|
|
}
|
|
} catch (e) {
|
|
showToast("Errore registrazione", 'error');
|
|
}
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
setState(prev => ({ ...prev, currentUser: null, userRole: 'guest', tickets: [] }));
|
|
showToast("Logout effettuato", 'info');
|
|
};
|
|
|
|
// --- Ticket Management ---
|
|
const createTicket = async (ticketData: Omit<Ticket, 'id' | 'createdAt' | 'messages' | 'status'>) => {
|
|
try {
|
|
const newTicket = await apiFetch('/tickets', {
|
|
method: 'POST',
|
|
body: JSON.stringify(ticketData)
|
|
});
|
|
setState(prev => ({ ...prev, tickets: [newTicket, ...prev.tickets] }));
|
|
showToast("Ticket creato correttamente", 'success');
|
|
} catch (e) {
|
|
showToast("Errore creazione ticket", 'error');
|
|
}
|
|
};
|
|
|
|
const replyToTicket = async (ticketId: string, message: string, attachments: Attachment[] = []) => {
|
|
const role = state.userRole === 'client' ? 'user' : 'assistant';
|
|
try {
|
|
const newMsg = await apiFetch(`/tickets/${ticketId}/messages`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ role, content: message, attachments })
|
|
});
|
|
|
|
setState(prev => ({
|
|
...prev,
|
|
tickets: prev.tickets.map(t => {
|
|
if (t.id !== ticketId) return t;
|
|
const shouldReopen = role === 'user' && t.status === TicketStatus.RESOLVED;
|
|
return {
|
|
...t,
|
|
messages: [...(t.messages || []), newMsg], // FIX: Ensure array fallback
|
|
status: shouldReopen ? TicketStatus.OPEN : t.status
|
|
};
|
|
})
|
|
}));
|
|
} catch (e) {
|
|
showToast("Errore invio messaggio", 'error');
|
|
}
|
|
};
|
|
|
|
const updateTicketStatus = async (id: string, status: TicketStatus) => {
|
|
try {
|
|
await apiFetch(`/tickets/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ status })
|
|
});
|
|
setState(prev => ({
|
|
...prev,
|
|
tickets: prev.tickets.map(t => t.id === id ? { ...t, status } : t)
|
|
}));
|
|
showToast(`Stato aggiornato a ${status}`, 'info');
|
|
} catch (e) {
|
|
showToast("Errore aggiornamento stato", 'error');
|
|
}
|
|
};
|
|
|
|
const updateTicketAgent = async (id: string, agentId: string) => {
|
|
try {
|
|
await apiFetch(`/tickets/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ assignedAgentId: agentId })
|
|
});
|
|
setState(prev => ({
|
|
...prev,
|
|
tickets: prev.tickets.map(t => t.id === id ? { ...t, assignedAgentId: agentId } : t)
|
|
}));
|
|
showToast("Agente assegnato", 'success');
|
|
} catch (e) {
|
|
showToast("Errore assegnazione agente", 'error');
|
|
}
|
|
};
|
|
|
|
const markTicketsAsAnalyzed = async (ticketIds: string[]) => {
|
|
try {
|
|
await apiFetch('/tickets/mark-analyzed', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ ticketIds })
|
|
});
|
|
// Optimistically update
|
|
setState(prev => ({
|
|
...prev,
|
|
tickets: prev.tickets.map(t => ticketIds.includes(t.id) ? { ...t, hasBeenAnalyzed: true } : t)
|
|
}));
|
|
} catch (e) {
|
|
console.error("Failed to mark tickets analyzed", e);
|
|
}
|
|
};
|
|
|
|
// --- KB Management ---
|
|
const addArticle = async (article: KBArticle) => {
|
|
try {
|
|
const res = await apiFetch('/articles', {
|
|
method: 'POST',
|
|
body: JSON.stringify(article)
|
|
});
|
|
setState(prev => ({
|
|
...prev,
|
|
articles: [{...article, id: res.id, lastUpdated: res.lastUpdated}, ...prev.articles]
|
|
}));
|
|
showToast("Articolo salvato", 'success');
|
|
} catch (e) {
|
|
showToast("Errore salvataggio articolo", 'error');
|
|
}
|
|
};
|
|
|
|
const updateArticle = async (updatedArticle: KBArticle) => {
|
|
try {
|
|
await apiFetch(`/articles/${updatedArticle.id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(updatedArticle)
|
|
});
|
|
setState(prev => ({
|
|
...prev,
|
|
articles: prev.articles.map(a => a.id === updatedArticle.id ? updatedArticle : a)
|
|
}));
|
|
showToast("Articolo aggiornato", 'success');
|
|
} catch (e) {
|
|
showToast("Errore aggiornamento articolo", 'error');
|
|
}
|
|
};
|
|
|
|
const deleteArticle = async (id: string) => {
|
|
try {
|
|
await apiFetch(`/articles/${id}`, { method: 'DELETE' });
|
|
setState(prev => ({
|
|
...prev,
|
|
articles: prev.articles.filter(a => a.id !== id)
|
|
}));
|
|
showToast("Articolo eliminato", 'info');
|
|
} catch(e) {
|
|
showToast("Errore eliminazione articolo", 'error');
|
|
}
|
|
};
|
|
|
|
// --- Survey Management ---
|
|
const submitSurvey = async (surveyData: Omit<SurveyResult, 'id' | 'timestamp'>) => {
|
|
try {
|
|
const res = await apiFetch('/surveys', {
|
|
method: 'POST',
|
|
body: JSON.stringify(surveyData)
|
|
});
|
|
const newSurvey = { ...surveyData, id: res.id, timestamp: new Date().toISOString() };
|
|
setState(prev => ({ ...prev, surveys: [...prev.surveys, newSurvey] }));
|
|
showToast("Feedback inviato", 'success');
|
|
} catch (e) {
|
|
showToast("Errore invio feedback", 'error');
|
|
}
|
|
};
|
|
|
|
// --- Settings & Management (Now with Persistence) ---
|
|
|
|
const addAgent = async (agent: Agent) => {
|
|
try {
|
|
await apiFetch('/agents', { method: 'POST', body: JSON.stringify(agent) });
|
|
setState(prev => ({ ...prev, agents: [...prev.agents, agent] }));
|
|
showToast("Agente creato", 'success');
|
|
} catch(e) { showToast("Errore creazione agente", 'error'); }
|
|
};
|
|
|
|
const updateAgent = async (agent: Agent) => {
|
|
try {
|
|
await apiFetch(`/agents/${agent.id}`, { method: 'PUT', body: JSON.stringify(agent) });
|
|
setState(prev => ({ ...prev, agents: prev.agents.map(a => a.id === agent.id ? agent : a) }));
|
|
showToast("Agente aggiornato", 'success');
|
|
} catch(e) { showToast("Errore aggiornamento agente", 'error'); }
|
|
};
|
|
|
|
const removeAgent = async (id: string) => {
|
|
try {
|
|
await apiFetch(`/agents/${id}`, { method: 'DELETE' });
|
|
setState(prev => ({ ...prev, agents: prev.agents.filter(a => a.id !== id) }));
|
|
showToast("Agente rimosso", 'info');
|
|
} catch(e) { showToast("Errore rimozione agente", 'error'); }
|
|
};
|
|
|
|
const addClientUser = async (user: ClientUser) => {
|
|
// Reuses auth/register usually, but for admin adding user:
|
|
try {
|
|
await apiFetch('/auth/register', { method: 'POST', body: JSON.stringify(user) });
|
|
setState(prev => ({ ...prev, clientUsers: [...prev.clientUsers, user] }));
|
|
showToast("Utente aggiunto", 'success');
|
|
} catch(e) { showToast("Errore aggiunta utente", 'error'); }
|
|
};
|
|
|
|
const updateClientUser = async (user: ClientUser) => {
|
|
try {
|
|
await apiFetch(`/client_users/${user.id}`, { method: 'PUT', body: JSON.stringify(user) });
|
|
setState(prev => ({ ...prev, clientUsers: prev.clientUsers.map(u => u.id === user.id ? user : u) }));
|
|
showToast("Utente aggiornato", 'success');
|
|
} catch(e) { showToast("Errore aggiornamento utente", 'error'); }
|
|
};
|
|
|
|
const removeClientUser = async (id: string) => {
|
|
try {
|
|
await apiFetch(`/client_users/${id}`, { method: 'DELETE' });
|
|
setState(prev => ({ ...prev, clientUsers: prev.clientUsers.filter(u => u.id !== id) }));
|
|
showToast("Utente rimosso", 'info');
|
|
} catch(e) { showToast("Errore rimozione utente", 'error'); }
|
|
};
|
|
|
|
const updateSettings = async (newSettings: AppSettings) => {
|
|
try {
|
|
await apiFetch('/settings', { method: 'PUT', body: JSON.stringify(newSettings) });
|
|
setState(prev => ({ ...prev, settings: newSettings }));
|
|
// Note: Real-time update of theme might require reload or context update, but setState handles it for current session
|
|
} catch(e) { showToast("Errore salvataggio impostazioni", 'error'); }
|
|
};
|
|
|
|
const addQueue = async (queue: TicketQueue) => {
|
|
try {
|
|
await apiFetch('/queues', { method: 'POST', body: JSON.stringify(queue) });
|
|
setState(prev => ({ ...prev, queues: [...prev.queues, queue] }));
|
|
showToast("Coda aggiunta", 'success');
|
|
} catch(e) { showToast("Errore aggiunta coda", 'error'); }
|
|
};
|
|
|
|
const removeQueue = async (id: string) => {
|
|
try {
|
|
await apiFetch(`/queues/${id}`, { method: 'DELETE' });
|
|
setState(prev => ({ ...prev, queues: prev.queues.filter(q => q.id !== id) }));
|
|
showToast("Coda rimossa", 'info');
|
|
} catch(e) { showToast("Errore rimozione coda", 'error'); }
|
|
};
|
|
|
|
|
|
// Render Logic
|
|
if (!state.currentUser) {
|
|
return (
|
|
<>
|
|
<AuthScreen
|
|
settings={state.settings}
|
|
onClientLogin={(e, p) => handleLogin(e, p, false)}
|
|
onAgentLogin={(e, p) => handleLogin(e, p, true)}
|
|
onClientRegister={handleClientRegister}
|
|
/>
|
|
<ToastContainer toasts={toasts} removeToast={removeToast} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Filter Tickets
|
|
const isAgentOrSupervisor = state.userRole === 'agent' || state.userRole === 'supervisor';
|
|
const agentTickets = isAgentOrSupervisor
|
|
? state.tickets.filter(t => (state.currentUser as Agent).queues?.includes(t.queue))
|
|
: state.tickets;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 text-gray-900 font-sans" style={{ '--brand-color': state.settings.branding.primaryColor } as React.CSSProperties}>
|
|
|
|
{state.userRole === 'client' ? (
|
|
<ClientPortal
|
|
currentUser={state.currentUser as ClientUser}
|
|
articles={state.settings.features.kbEnabled ? state.articles : []}
|
|
tickets={state.tickets.filter(t => t.customerName === state.currentUser?.name)}
|
|
queues={state.queues}
|
|
settings={state.settings}
|
|
onCreateTicket={createTicket}
|
|
onReplyTicket={replyToTicket}
|
|
onSubmitSurvey={submitSurvey}
|
|
onLogout={handleLogout}
|
|
showToast={showToast}
|
|
/>
|
|
) : (
|
|
<AgentDashboard
|
|
currentUser={state.currentUser as Agent}
|
|
tickets={agentTickets}
|
|
articles={state.settings.features.kbEnabled ? state.articles : []}
|
|
agents={state.agents}
|
|
queues={state.queues}
|
|
surveys={state.surveys}
|
|
clientUsers={state.clientUsers}
|
|
settings={state.settings}
|
|
updateTicketStatus={updateTicketStatus}
|
|
updateTicketAgent={updateTicketAgent}
|
|
onReplyTicket={replyToTicket}
|
|
addArticle={addArticle}
|
|
updateArticle={updateArticle}
|
|
deleteArticle={deleteArticle} // New Prop
|
|
markTicketsAsAnalyzed={markTicketsAsAnalyzed} // New Prop
|
|
addAgent={addAgent}
|
|
updateAgent={updateAgent}
|
|
removeAgent={removeAgent}
|
|
addClientUser={addClientUser}
|
|
updateClientUser={updateClientUser}
|
|
removeClientUser={removeClientUser}
|
|
updateSettings={updateSettings}
|
|
addQueue={addQueue}
|
|
removeQueue={removeQueue}
|
|
onLogout={handleLogout}
|
|
showToast={showToast}
|
|
/>
|
|
)}
|
|
<ToastContainer toasts={toasts} removeToast={removeToast} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|