diff --git a/App.tsx b/App.tsx index f8ba8f9..231cbe2 100644 --- a/App.tsx +++ b/App.tsx @@ -1,25 +1,83 @@ -import React, { useState } from 'react'; +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_KB, INITIAL_QUEUES, INITIAL_SETTINGS, INITIAL_TICKETS, MOCK_AGENTS, MOCK_CLIENT_USERS, MOCK_SURVEYS } from './constants'; -import { Agent, AppSettings, AppState, ClientUser, KBArticle, Ticket, TicketStatus, SurveyResult, TicketQueue, ChatMessage } from './types'; +import { INITIAL_SETTINGS } from './constants'; +import { Agent, AppSettings, AppState, ClientUser, KBArticle, Ticket, TicketStatus, SurveyResult, TicketQueue, ChatMessage, AgentRole } from './types'; const App: React.FC = () => { const [state, setState] = useState({ - tickets: INITIAL_TICKETS, - articles: INITIAL_KB, - agents: MOCK_AGENTS, - queues: INITIAL_QUEUES, - surveys: MOCK_SURVEYS, - clientUsers: MOCK_CLIENT_USERS, + tickets: [], + articles: [], + agents: [], + queues: [], + surveys: [], + clientUsers: [], settings: INITIAL_SETTINGS, currentUser: null, userRole: 'guest' }); const [toasts, setToasts] = useState([]); + 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 id = Date.now().toString(); @@ -31,213 +89,181 @@ const App: React.FC = () => { }; // --- Auth Management --- - const handleClientLogin = (email: string, pass: string): boolean => { - const user = state.clientUsers.find(u => u.email === email && u.password === pass); - if (user) { - setState(prev => ({ ...prev, currentUser: user, userRole: 'client' })); - showToast(`Bentornato, ${user.name}!`, 'success'); - return true; + 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; } - return false; }; - const handleAgentLogin = (email: string, pass: string): boolean => { - const agent = state.agents.find(a => a.email === email && a.password === pass); - if (agent) { - // Set the specific role from the agent object (agent, supervisor, superadmin) - setState(prev => ({ ...prev, currentUser: agent, userRole: agent.role })); - showToast(`Accesso effettuato come ${agent.role}`, 'success'); - return true; + 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'); } - return false; - }; - - const handleClientRegister = (name: string, email: string, pass: string, company: string) => { - const newUser: ClientUser = { - id: `u${Date.now()}`, - name, - email, - password: pass, - company, - status: 'active' - }; - setState(prev => ({ - ...prev, - clientUsers: [...prev.clientUsers, newUser], - currentUser: newUser, - userRole: 'client' - })); - showToast("Registrazione completata con successo!", 'success'); }; const handleLogout = () => { - setState(prev => ({ ...prev, currentUser: null, userRole: 'guest' })); + setState(prev => ({ ...prev, currentUser: null, userRole: 'guest', tickets: [] })); showToast("Logout effettuato", 'info'); }; // --- Ticket Management --- - const createTicket = (ticketData: Omit) => { - const newTicket: Ticket = { - ...ticketData, - id: `T-${1000 + state.tickets.length + 1}`, - createdAt: new Date().toISOString(), - status: TicketStatus.OPEN, - messages: [], - attachments: ticketData.attachments || [] - }; - setState(prev => ({ ...prev, tickets: [newTicket, ...prev.tickets] })); - showToast("Ticket creato correttamente", 'success'); + const createTicket = async (ticketData: Omit) => { + 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 = (ticketId: string, message: string) => { - const newMessage: ChatMessage = { - id: `m-${Date.now()}`, - role: 'user', - content: message, - timestamp: new Date().toISOString() - }; + const replyToTicket = async (ticketId: string, message: string) => { + const role = state.userRole === 'client' ? 'user' : 'assistant'; + try { + const newMsg = await apiFetch(`/tickets/${ticketId}/messages`, { + method: 'POST', + body: JSON.stringify({ role, content: message }) + }); - setState(prev => ({ - ...prev, - tickets: prev.tickets.map(t => - t.id === ticketId - ? { ...t, messages: [...t.messages, newMessage], status: t.status === TicketStatus.RESOLVED ? TicketStatus.OPEN : t.status } - : t - ) - })); - // Toast handled in component for better UX or here + // Update local state optimistically or via re-fetch + setState(prev => ({ + ...prev, + tickets: prev.tickets.map(t => { + if (t.id !== ticketId) return t; + // Se l'utente risponde, riapri il ticket se era risolto (gestito anche dal backend) + const shouldReopen = role === 'user' && t.status === TicketStatus.RESOLVED; + return { + ...t, + messages: [...t.messages, newMsg], + status: shouldReopen ? TicketStatus.OPEN : t.status + }; + }) + })); + } catch (e) { + showToast("Errore invio messaggio", 'error'); + } }; - const updateTicketStatus = (id: string, status: TicketStatus) => { - setState(prev => ({ - ...prev, - tickets: prev.tickets.map(t => t.id === id ? { ...t, status } : t) - })); - showToast(`Stato ticket aggiornato a ${status}`, 'info'); + 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 = (id: string, agentId: string) => { - setState(prev => ({ - ...prev, - tickets: prev.tickets.map(t => t.id === id ? { ...t, assignedAgentId: agentId } : t) - })); - showToast("Agente assegnato con successo", 'success'); + 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'); + } }; // --- KB Management --- - const addArticle = (article: KBArticle) => { - if (!state.settings.features.kbEnabled) { - showToast("La funzionalità Knowledge Base è disabilitata.", 'error'); - return; + 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'); } - 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) => { - setState(prev => ({ - ...prev, - articles: prev.articles.map(a => a.id === updatedArticle.id ? updatedArticle : a) - })); - showToast("Articolo aggiornato", 'success'); + 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'); + } }; // --- Survey Management --- - const submitSurvey = (surveyData: Omit) => { - const newSurvey: SurveyResult = { - ...surveyData, - id: `s${Date.now()}`, - timestamp: new Date().toISOString() - }; - setState(prev => ({ - ...prev, - surveys: [...prev.surveys, newSurvey] - })); - showToast("Grazie per il tuo feedback!", 'success'); - }; - - // --- 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; - } + const submitSurvey = async (surveyData: Omit) => { + 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'); } - - setState(prev => ({ ...prev, agents: [...prev.agents, agent] })); - showToast("Nuovo agente aggiunto", 'success'); }; - const updateAgent = (agent: Agent) => { - setState(prev => ({ ...prev, agents: prev.agents.map(a => a.id === agent.id ? agent : a) })); - showToast("Dati agente aggiornati", 'success'); - }; - - const removeAgent = (id: string) => { - setState(prev => ({ ...prev, agents: prev.agents.filter(a => a.id !== id) })); - showToast("Agente rimosso", 'info'); - }; - - 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'); - }; + // --- Stub functions for settings (implement API if needed) --- + const addAgent = (agent: Agent) => { setState(prev => ({ ...prev, agents: [...prev.agents, agent] })); }; + 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 updateClientUser = (user: ClientUser) => { setState(prev => ({ ...prev, clientUsers: prev.clientUsers.map(u => u.id === user.id ? user : u) })); }; + const removeClientUser = (id: string) => { setState(prev => ({ ...prev, clientUsers: prev.clientUsers.filter(u => u.id !== id) })); }; + 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) })); }; // Render Logic @@ -246,8 +272,8 @@ const App: React.FC = () => { <> handleLogin(e, p, false)} + onAgentLogin={(e, p) => handleLogin(e, p, true)} onClientRegister={handleClientRegister} /> @@ -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 agentTickets = isAgentOrSupervisor - ? state.tickets.filter(t => (state.currentUser as Agent).queues.includes(t.queue)) - : state.tickets; // Superadmin sees all + ? state.tickets.filter(t => (state.currentUser as Agent).queues?.includes(t.queue)) + : state.tickets; return (
@@ -270,7 +296,7 @@ const App: React.FC = () => { articles={state.settings.features.kbEnabled ? state.articles : []} tickets={state.tickets.filter(t => t.customerName === state.currentUser?.name)} queues={state.queues} - settings={state.settings} // Passed settings + settings={state.settings} onCreateTicket={createTicket} onReplyTicket={replyToTicket} onSubmitSurvey={submitSurvey}