Update App.tsx

This commit is contained in:
fcarraUniSa
2026-02-17 09:52:52 +01:00
committed by GitHub
parent 1abd426b79
commit 4a1641b0e2

410
App.tsx
View File

@@ -1,25 +1,83 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { AgentDashboard } from './components/AgentDashboard'; import { AgentDashboard } from './components/AgentDashboard';
import { ClientPortal } from './components/ClientPortal'; import { ClientPortal } from './components/ClientPortal';
import { AuthScreen } from './components/AuthScreen'; import { AuthScreen } from './components/AuthScreen';
import { ToastContainer, ToastMessage, ToastType } from './components/Toast'; import { ToastContainer, ToastMessage, ToastType } from './components/Toast';
import { INITIAL_KB, INITIAL_QUEUES, INITIAL_SETTINGS, INITIAL_TICKETS, MOCK_AGENTS, MOCK_CLIENT_USERS, MOCK_SURVEYS } from './constants'; import { INITIAL_SETTINGS } from './constants';
import { Agent, AppSettings, AppState, ClientUser, KBArticle, Ticket, TicketStatus, SurveyResult, TicketQueue, ChatMessage } from './types'; import { Agent, AppSettings, AppState, ClientUser, KBArticle, Ticket, TicketStatus, SurveyResult, TicketQueue, ChatMessage, AgentRole } from './types';
const App: React.FC = () => { const App: React.FC = () => {
const [state, setState] = useState<AppState>({ const [state, setState] = useState<AppState>({
tickets: INITIAL_TICKETS, tickets: [],
articles: INITIAL_KB, articles: [],
agents: MOCK_AGENTS, agents: [],
queues: INITIAL_QUEUES, queues: [],
surveys: MOCK_SURVEYS, surveys: [],
clientUsers: MOCK_CLIENT_USERS, clientUsers: [],
settings: INITIAL_SETTINGS, settings: INITIAL_SETTINGS,
currentUser: null, currentUser: null,
userRole: 'guest' userRole: 'guest'
}); });
const [toasts, setToasts] = useState<ToastMessage[]>([]); 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
}));
} catch (e) {
showToast("Errore caricamento dati dal server", 'error');
} finally {
setLoading(false);
}
};
useEffect(() => {
// Carica i dati all'avvio solo se l'utente è loggato, oppure carica dati pubblici se necessario.
// In questo design, carichiamo tutto dopo il login per sicurezza, o pre-fetch.
// Per semplicità, carichiamo tutto all'avvio per popolare le liste, ma in prod si farebbe dopo auth.
// Qui chiamiamo loadData solo se c'è un utente, altrimenti auth screen
}, []);
// Effect to refresh tickets periodically if logged in
useEffect(() => {
if (state.currentUser) {
loadData();
const interval = setInterval(loadData, 10000); // Poll every 10s
return () => clearInterval(interval);
}
}, [state.currentUser]);
const showToast = (message: string, type: ToastType = 'info') => { const showToast = (message: string, type: ToastType = 'info') => {
const id = Date.now().toString(); const id = Date.now().toString();
@@ -31,213 +89,181 @@ const App: React.FC = () => {
}; };
// --- Auth Management --- // --- Auth Management ---
const handleClientLogin = (email: string, pass: string): boolean => { const handleLogin = async (email: string, pass: string, isAgent: boolean) => {
const user = state.clientUsers.find(u => u.email === email && u.password === pass); try {
if (user) { const res = await apiFetch('/auth/login', {
setState(prev => ({ ...prev, currentUser: user, userRole: 'client' })); method: 'POST',
showToast(`Bentornato, ${user.name}!`, 'success'); body: JSON.stringify({ email, password: pass })
return true; });
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;
} }
return false;
}; };
const handleAgentLogin = (email: string, pass: string): boolean => { const handleClientRegister = async (name: string, email: string, pass: string, company: string) => {
const agent = state.agents.find(a => a.email === email && a.password === pass); try {
if (agent) { const res = await apiFetch('/auth/register', {
// Set the specific role from the agent object (agent, supervisor, superadmin) method: 'POST',
setState(prev => ({ ...prev, currentUser: agent, userRole: agent.role })); body: JSON.stringify({ name, email, password: pass, company })
showToast(`Accesso effettuato come ${agent.role}`, 'success'); });
return true;
}
return false;
};
const handleClientRegister = (name: string, email: string, pass: string, company: string) => { if (res.success) {
const newUser: ClientUser = { setState(prev => ({
id: `u${Date.now()}`, ...prev,
name, clientUsers: [...prev.clientUsers, res.user],
email, currentUser: res.user,
password: pass, userRole: 'client'
company, }));
status: 'active' showToast("Registrazione completata!", 'success');
}; }
setState(prev => ({ } catch (e) {
...prev, showToast("Errore registrazione", 'error');
clientUsers: [...prev.clientUsers, newUser], }
currentUser: newUser,
userRole: 'client'
}));
showToast("Registrazione completata con successo!", 'success');
}; };
const handleLogout = () => { const handleLogout = () => {
setState(prev => ({ ...prev, currentUser: null, userRole: 'guest' })); setState(prev => ({ ...prev, currentUser: null, userRole: 'guest', tickets: [] }));
showToast("Logout effettuato", 'info'); showToast("Logout effettuato", 'info');
}; };
// --- Ticket Management --- // --- Ticket Management ---
const createTicket = (ticketData: Omit<Ticket, 'id' | 'createdAt' | 'messages' | 'status'>) => { const createTicket = async (ticketData: Omit<Ticket, 'id' | 'createdAt' | 'messages' | 'status'>) => {
const newTicket: Ticket = { try {
...ticketData, const newTicket = await apiFetch('/tickets', {
id: `T-${1000 + state.tickets.length + 1}`, method: 'POST',
createdAt: new Date().toISOString(), body: JSON.stringify(ticketData)
status: TicketStatus.OPEN, });
messages: [], setState(prev => ({ ...prev, tickets: [newTicket, ...prev.tickets] }));
attachments: ticketData.attachments || [] showToast("Ticket creato correttamente", 'success');
}; } catch (e) {
setState(prev => ({ ...prev, tickets: [newTicket, ...prev.tickets] })); showToast("Errore creazione ticket", 'error');
showToast("Ticket creato correttamente", 'success'); }
}; };
const replyToTicket = (ticketId: string, message: string) => { const replyToTicket = async (ticketId: string, message: string) => {
const newMessage: ChatMessage = { const role = state.userRole === 'client' ? 'user' : 'assistant';
id: `m-${Date.now()}`, try {
role: 'user', const newMsg = await apiFetch(`/tickets/${ticketId}/messages`, {
content: message, method: 'POST',
timestamp: new Date().toISOString() body: JSON.stringify({ role, content: message })
}; });
setState(prev => ({ // Update local state optimistically or via re-fetch
...prev, setState(prev => ({
tickets: prev.tickets.map(t => ...prev,
t.id === ticketId tickets: prev.tickets.map(t => {
? { ...t, messages: [...t.messages, newMessage], status: t.status === TicketStatus.RESOLVED ? TicketStatus.OPEN : t.status } if (t.id !== ticketId) return t;
: t // Se l'utente risponde, riapri il ticket se era risolto (gestito anche dal backend)
) const shouldReopen = role === 'user' && t.status === TicketStatus.RESOLVED;
})); return {
// Toast handled in component for better UX or here ...t,
messages: [...t.messages, newMsg],
status: shouldReopen ? TicketStatus.OPEN : t.status
};
})
}));
} catch (e) {
showToast("Errore invio messaggio", 'error');
}
}; };
const updateTicketStatus = (id: string, status: TicketStatus) => { const updateTicketStatus = async (id: string, status: TicketStatus) => {
setState(prev => ({ try {
...prev, await apiFetch(`/tickets/${id}`, {
tickets: prev.tickets.map(t => t.id === id ? { ...t, status } : t) method: 'PATCH',
})); body: JSON.stringify({ status })
showToast(`Stato ticket aggiornato a ${status}`, 'info'); });
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 = (id: string, agentId: string) => { const updateTicketAgent = async (id: string, agentId: string) => {
setState(prev => ({ try {
...prev, await apiFetch(`/tickets/${id}`, {
tickets: prev.tickets.map(t => t.id === id ? { ...t, assignedAgentId: agentId } : t) method: 'PATCH',
})); body: JSON.stringify({ assignedAgentId: agentId })
showToast("Agente assegnato con successo", 'success'); });
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');
}
}; };
// --- KB Management --- // --- KB Management ---
const addArticle = (article: KBArticle) => { const addArticle = async (article: KBArticle) => {
if (!state.settings.features.kbEnabled) { try {
showToast("La funzionalità Knowledge Base è disabilitata.", 'error'); const res = await apiFetch('/articles', {
return; 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');
} }
if (state.articles.length >= state.settings.features.maxKbArticles) {
showToast(`Limite massimo di articoli (${state.settings.features.maxKbArticles}) raggiunto.`, 'error');
return;
}
// Check for AI Quota if manually triggering AI generation logic
if (article.source === 'ai') {
if (!state.settings.features.aiKnowledgeAgentEnabled) {
showToast("L'Agente AI per la Knowledge Base è disabilitato.", 'error');
return;
}
const aiCount = state.articles.filter(a => a.source === 'ai').length;
if (aiCount >= state.settings.features.maxAiGeneratedArticles) {
showToast(`Limite creazione articoli AI (${state.settings.features.maxAiGeneratedArticles}) raggiunto.`, 'error');
return;
}
}
setState(prev => ({
...prev,
articles: [article, ...prev.articles]
}));
showToast("Articolo aggiunto con successo", 'success');
}; };
const updateArticle = (updatedArticle: KBArticle) => { const updateArticle = async (updatedArticle: KBArticle) => {
setState(prev => ({ try {
...prev, await apiFetch(`/articles/${updatedArticle.id}`, {
articles: prev.articles.map(a => a.id === updatedArticle.id ? updatedArticle : a) method: 'PATCH',
})); body: JSON.stringify(updatedArticle)
showToast("Articolo aggiornato", 'success'); });
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');
}
}; };
// --- Survey Management --- // --- Survey Management ---
const submitSurvey = (surveyData: Omit<SurveyResult, 'id' | 'timestamp'>) => { const submitSurvey = async (surveyData: Omit<SurveyResult, 'id' | 'timestamp'>) => {
const newSurvey: SurveyResult = { try {
...surveyData, const res = await apiFetch('/surveys', {
id: `s${Date.now()}`, method: 'POST',
timestamp: new Date().toISOString() body: JSON.stringify(surveyData)
}; });
setState(prev => ({ const newSurvey = { ...surveyData, id: res.id, timestamp: new Date().toISOString() };
...prev, setState(prev => ({ ...prev, surveys: [...prev.surveys, newSurvey] }));
surveys: [...prev.surveys, newSurvey] showToast("Feedback inviato", 'success');
})); } catch (e) {
showToast("Grazie per il tuo feedback!", 'success'); showToast("Errore invio feedback", 'error');
};
// --- Settings Management ---
const addAgent = (agent: Agent) => {
// Quota Validation
if (agent.role === 'supervisor') {
const supervisorCount = state.agents.filter(a => a.role === 'supervisor').length;
if (supervisorCount >= state.settings.features.maxSupervisors) {
showToast(`Limite Supervisor (${state.settings.features.maxSupervisors}) raggiunto.`, 'error');
return;
}
} else if (agent.role === 'agent') {
const agentCount = state.agents.filter(a => a.role === 'agent').length;
if (agentCount >= state.settings.features.maxAgents) {
showToast(`Limite Agenti (${state.settings.features.maxAgents}) raggiunto.`, 'error');
return;
}
} }
setState(prev => ({ ...prev, agents: [...prev.agents, agent] }));
showToast("Nuovo agente aggiunto", 'success');
}; };
const updateAgent = (agent: Agent) => { // --- Stub functions for settings (implement API if needed) ---
setState(prev => ({ ...prev, agents: prev.agents.map(a => a.id === agent.id ? agent : a) })); const addAgent = (agent: Agent) => { setState(prev => ({ ...prev, agents: [...prev.agents, agent] })); };
showToast("Dati agente aggiornati", 'success'); const updateAgent = (agent: Agent) => { setState(prev => ({ ...prev, agents: prev.agents.map(a => a.id === agent.id ? agent : a) })); };
}; const removeAgent = (id: string) => { setState(prev => ({ ...prev, agents: prev.agents.filter(a => a.id !== id) })); };
const addClientUser = (user: ClientUser) => { setState(prev => ({ ...prev, clientUsers: [...prev.clientUsers, user] })); };
const removeAgent = (id: string) => { const updateClientUser = (user: ClientUser) => { setState(prev => ({ ...prev, clientUsers: prev.clientUsers.map(u => u.id === user.id ? user : u) })); };
setState(prev => ({ ...prev, agents: prev.agents.filter(a => a.id !== id) })); const removeClientUser = (id: string) => { setState(prev => ({ ...prev, clientUsers: prev.clientUsers.filter(u => u.id !== id) })); };
showToast("Agente rimosso", 'info'); const updateSettings = (newSettings: AppSettings) => { setState(prev => ({ ...prev, settings: newSettings })); };
}; const addQueue = (queue: TicketQueue) => { setState(prev => ({ ...prev, queues: [...prev.queues, queue] })); };
const removeQueue = (id: string) => { setState(prev => ({ ...prev, queues: prev.queues.filter(q => q.id !== id) })); };
const addClientUser = (user: ClientUser) => {
setState(prev => ({ ...prev, clientUsers: [...prev.clientUsers, user] }));
showToast("Utente aggiunto", 'success');
};
const updateClientUser = (user: ClientUser) => {
setState(prev => ({ ...prev, clientUsers: prev.clientUsers.map(u => u.id === user.id ? user : u) }));
showToast("Utente aggiornato", 'success');
};
const removeClientUser = (id: string) => {
setState(prev => ({ ...prev, clientUsers: prev.clientUsers.filter(u => u.id !== id) }));
showToast("Utente rimosso", 'info');
};
const updateSettings = (newSettings: AppSettings) => {
setState(prev => ({ ...prev, settings: newSettings }));
// Toast triggered in component
};
// --- Queue Management ---
const addQueue = (queue: TicketQueue) => {
setState(prev => ({ ...prev, queues: [...prev.queues, queue] }));
showToast("Coda creata", 'success');
};
const removeQueue = (id: string) => {
setState(prev => ({ ...prev, queues: prev.queues.filter(q => q.id !== id) }));
showToast("Coda rimossa", 'info');
};
// Render Logic // Render Logic
@@ -246,8 +272,8 @@ const App: React.FC = () => {
<> <>
<AuthScreen <AuthScreen
settings={state.settings} settings={state.settings}
onClientLogin={handleClientLogin} onClientLogin={(e, p) => handleLogin(e, p, false)}
onAgentLogin={handleAgentLogin} onAgentLogin={(e, p) => handleLogin(e, p, true)}
onClientRegister={handleClientRegister} onClientRegister={handleClientRegister}
/> />
<ToastContainer toasts={toasts} removeToast={removeToast} /> <ToastContainer toasts={toasts} removeToast={removeToast} />
@@ -255,11 +281,11 @@ const App: React.FC = () => {
); );
} }
// Filter Tickets for Agent: Only show tickets from assigned queues // Filter Tickets
const isAgentOrSupervisor = state.userRole === 'agent' || state.userRole === 'supervisor'; const isAgentOrSupervisor = state.userRole === 'agent' || state.userRole === 'supervisor';
const agentTickets = isAgentOrSupervisor const agentTickets = isAgentOrSupervisor
? state.tickets.filter(t => (state.currentUser as Agent).queues.includes(t.queue)) ? state.tickets.filter(t => (state.currentUser as Agent).queues?.includes(t.queue))
: state.tickets; // Superadmin sees all : state.tickets;
return ( return (
<div className="min-h-screen bg-gray-50 text-gray-900 font-sans" style={{ '--brand-color': state.settings.branding.primaryColor } as React.CSSProperties}> <div className="min-h-screen bg-gray-50 text-gray-900 font-sans" style={{ '--brand-color': state.settings.branding.primaryColor } as React.CSSProperties}>
@@ -270,7 +296,7 @@ const App: React.FC = () => {
articles={state.settings.features.kbEnabled ? state.articles : []} articles={state.settings.features.kbEnabled ? state.articles : []}
tickets={state.tickets.filter(t => t.customerName === state.currentUser?.name)} tickets={state.tickets.filter(t => t.customerName === state.currentUser?.name)}
queues={state.queues} queues={state.queues}
settings={state.settings} // Passed settings settings={state.settings}
onCreateTicket={createTicket} onCreateTicket={createTicket}
onReplyTicket={replyToTicket} onReplyTicket={replyToTicket}
onSubmitSurvey={submitSurvey} onSubmitSurvey={submitSurvey}