From ea8747e8588440db899794d66c98b140652e271d Mon Sep 17 00:00:00 2001 From: "hamza.bouslama" <hamza.bouslama@marketingconfort.com> Date: Sat, 17 May 2025 21:56:57 +0100 Subject: [PATCH] Full configuration for chatbot context , applying role based model functionality , token configuration , testing logic --- .env | 3 +- docker-compose.yml | 2 +- .../conversation/page.tsx | 0 .../{support => supportagent}/page.tsx | 0 .../conversation/page.tsx | 0 .../history/[id]/page.tsx | 0 .../chatbot/{ticket => ticketagent}/page.tsx | 0 src/config-global.ts | 4 + src/contexts/auth/guard/auth-guard.tsx | 1 + src/contexts/auth/guard/guest-guard.tsx | 1 + src/contexts/auth/jwt/auth-provider.tsx | 1 + src/routes/paths.ts | 12 +- src/shared/api/chat-bot.ts | 65 +- src/shared/api/server.ts | 117 ++-- src/shared/layouts/common/account-popover.tsx | 1 + .../layouts/dashboard/config-navigation.tsx | 63 +- .../sections/auth/jwt/jwt-login-view.tsx | 23 +- .../chat-bot/faq/view/faq-list-view.tsx | 11 +- .../chat-bot/help/help-item-horizontal.tsx | 2 +- .../chat-bot/help/view/help-list-view.tsx | 2 +- .../chat-bot/newchat/chat-empty-state.tsx | 7 - .../sections/chat-bot/newchat/chat-layout.tsx | 11 +- .../sections/chat-bot/newchat/chat-list.tsx | 116 ++-- .../chat-bot/newchat/chat-message-input.tsx | 20 +- .../chat-bot/newchat/chat-room-single.tsx | 7 +- .../sections/chat-bot/newchat/chat-room.tsx | 8 +- .../chat-bot/newchat/view/chat-view.tsx | 161 +++-- .../support/view/support-list-view.tsx | 18 +- .../chat-bot/ticket/ticket-table-row.tsx | 62 +- .../ticket/view/ticket-history-data-view.tsx | 645 +++++++++++++++--- .../chat-bot/ticket/view/ticket-list-view.tsx | 70 +- src/shared/types/user.ts | 2 + src/utils/token.ts | 17 + 33 files changed, 1072 insertions(+), 380 deletions(-) rename src/app/dashboard/chatbot/{support => supportagent}/conversation/page.tsx (100%) rename src/app/dashboard/chatbot/{support => supportagent}/page.tsx (100%) rename src/app/dashboard/chatbot/{ticket => ticketagent}/conversation/page.tsx (100%) rename src/app/dashboard/chatbot/{ticket => ticketagent}/history/[id]/page.tsx (100%) rename src/app/dashboard/chatbot/{ticket => ticketagent}/page.tsx (100%) diff --git a/.env b/.env index 42df6545..f62745b4 100644 --- a/.env +++ b/.env @@ -37,13 +37,12 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY= NEXT_PUBLIC_WEBSOCKET_URL= NEXT_PUBLIC_DEFAULT_IMAGE_URL=https://mydressin-rec.s3.eu-west-3.amazonaws.com/Image-not-found.png +NEXT_PUBLIC_BASE_URL_WEB_SOCKET_URL=ws://api.mydressin-server.com/api/chatbot/ws/chat #GATEWAY API URL NEXT_PUBLIC_MYDRESSIN_GATEWAY_API_URL=https://api.mydressin-server.com NEXT_PUBLIC_MYDRESSIN_CLIENT_URL=https://app.mydressin-server.com - - NEXT_ENCRYPTION_KEY=market1ngconf0rT NEXT_ENCRYPTION_IV=mark5t1ngconf0rT diff --git a/docker-compose.yml b/docker-compose.yml index eac39e41..bd9c3325 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: app: - image: mydressin-front-integrating:rec + image: marketingconfort/mydressin-front-integrating:rec container_name: mydressin-front-integrating restart: always build: diff --git a/src/app/dashboard/chatbot/support/conversation/page.tsx b/src/app/dashboard/chatbot/supportagent/conversation/page.tsx similarity index 100% rename from src/app/dashboard/chatbot/support/conversation/page.tsx rename to src/app/dashboard/chatbot/supportagent/conversation/page.tsx diff --git a/src/app/dashboard/chatbot/support/page.tsx b/src/app/dashboard/chatbot/supportagent/page.tsx similarity index 100% rename from src/app/dashboard/chatbot/support/page.tsx rename to src/app/dashboard/chatbot/supportagent/page.tsx diff --git a/src/app/dashboard/chatbot/ticket/conversation/page.tsx b/src/app/dashboard/chatbot/ticketagent/conversation/page.tsx similarity index 100% rename from src/app/dashboard/chatbot/ticket/conversation/page.tsx rename to src/app/dashboard/chatbot/ticketagent/conversation/page.tsx diff --git a/src/app/dashboard/chatbot/ticket/history/[id]/page.tsx b/src/app/dashboard/chatbot/ticketagent/history/[id]/page.tsx similarity index 100% rename from src/app/dashboard/chatbot/ticket/history/[id]/page.tsx rename to src/app/dashboard/chatbot/ticketagent/history/[id]/page.tsx diff --git a/src/app/dashboard/chatbot/ticket/page.tsx b/src/app/dashboard/chatbot/ticketagent/page.tsx similarity index 100% rename from src/app/dashboard/chatbot/ticket/page.tsx rename to src/app/dashboard/chatbot/ticketagent/page.tsx diff --git a/src/config-global.ts b/src/config-global.ts index 724f9ee1..ba2d8999 100644 --- a/src/config-global.ts +++ b/src/config-global.ts @@ -50,6 +50,10 @@ export const PATH_AFTER_LOGIN_STOCK_MANAGER = paths.dashboard.admin.product.root export const PATH_AFTER_LOGIN_GESTIONNAIRE_DE_PRECO = paths.dashboard.admin.product.root; export const PATH_AFTER_LOGIN_GESTIONNAIRE_RETOUR = paths.dashboard.admin.order.all_orders; +// ROOT PATH AFTER LOGIN SUCCESSFUL FOR CHATBOT +export const PATH_AFTER_LOGIN_LEADER = paths.dashboard.chatbot.ticket.root; +export const PATH_AFTER_LOGIN_SUPPORT = paths.dashboard.chatbot.support.root; + diff --git a/src/contexts/auth/guard/auth-guard.tsx b/src/contexts/auth/guard/auth-guard.tsx index 5866d8f4..a5b36ae2 100644 --- a/src/contexts/auth/guard/auth-guard.tsx +++ b/src/contexts/auth/guard/auth-guard.tsx @@ -36,6 +36,7 @@ function Container({ children }: Props) { const handleLogout = useCallback(() => { localStorage.removeItem("token"); + localStorage.removeItem("chatbotToken"); localStorage.removeItem("refreshToken"); localStorage.removeItem("expirationDurationInSec"); router.push(loginPaths.jwt); diff --git a/src/contexts/auth/guard/guest-guard.tsx b/src/contexts/auth/guard/guest-guard.tsx index 0c0d1d66..06804021 100644 --- a/src/contexts/auth/guard/guest-guard.tsx +++ b/src/contexts/auth/guard/guest-guard.tsx @@ -37,6 +37,7 @@ function Container({ children }: Props) { } else { setChecked(false); localStorage.removeItem("token"); + localStorage.removeItem("chatbotToken"); localStorage.removeItem("refreshToken"); localStorage.removeItem("expirationDurationInSec"); router.push("/auth/jwt/login") diff --git a/src/contexts/auth/jwt/auth-provider.tsx b/src/contexts/auth/jwt/auth-provider.tsx index 24ddeac5..7cc145f7 100644 --- a/src/contexts/auth/jwt/auth-provider.tsx +++ b/src/contexts/auth/jwt/auth-provider.tsx @@ -57,6 +57,7 @@ export const AuthProvider = ({ children }: Props) => { const result = await ChangingLogingActivity(requestLoginActivity); if (result.status === 200) { localStorage.removeItem("token"); + localStorage.removeItem("chatbotToken"); localStorage.removeItem("refreshToken"); localStorage.removeItem("expirationDurationInSec"); router.push("/auth/jwt/login"); diff --git a/src/routes/paths.ts b/src/routes/paths.ts index e3a52e7d..2da4a821 100644 --- a/src/routes/paths.ts +++ b/src/routes/paths.ts @@ -226,14 +226,14 @@ export const paths = { list : (id: string) => `${ROOTS.DASHBOARD}/chatbot/openai/${id? id : '1'}`, }, ticket: { - root: `${ROOTS.DASHBOARD}/chatbot/ticket`, - history: `${ROOTS.DASHBOARD}/chatbot/ticket/history`, - conversations : `${ROOTS.DASHBOARD}/chatbot/ticket/conversation`, - view: (id: string) => `${ROOTS.DASHBOARD}/chatbot/ticket/history/${id}`, + root: `${ROOTS.DASHBOARD}/chatbot/ticketagent`, + history: `${ROOTS.DASHBOARD}/chatbot/ticketagent/history`, + conversations : `${ROOTS.DASHBOARD}/chatbot/ticketagent/conversation`, + view: (id: string) => `${ROOTS.DASHBOARD}/chatbot/ticketagent/history/${id}`, }, support: { - root: `${ROOTS.DASHBOARD}/chatbot/support`, - conversation: `${ROOTS.DASHBOARD}/chatbot/support/conversation`, + root: `${ROOTS.DASHBOARD}/chatbot/supportagent`, + conversation: `${ROOTS.DASHBOARD}/chatbot/supportagent/conversation`, }, } }, diff --git a/src/shared/api/chat-bot.ts b/src/shared/api/chat-bot.ts index 593cdeeb..6d744683 100644 --- a/src/shared/api/chat-bot.ts +++ b/src/shared/api/chat-bot.ts @@ -3,7 +3,8 @@ import { useMemo } from 'react'; import useSWR, { mutate } from 'swr'; import { ITiketItem } from '../types/ticket'; -import { fetcherchatbot , endpoints } from './server'; +import axios, { fetcherchatbot , endpoints } from './server'; +import { isValidToken } from '@/utils/token'; // ---------------------------------------------------------------------- @@ -83,24 +84,18 @@ export function useGetTicketByConversation(conversationId: string) { } // ---------------------------------------------------------------------- -export function useGetAllUsers() { - const url = endpoints.chatbot.user.list; - const { data, isLoading, error, isValidating } = useSWR(url,fetcherchatbot,swrOptions); - - const memoizedValue = useMemo( - () => ({ - users: data, - usersLoading: isLoading, - usersError: error, - usersValidating: isValidating, - usersEmpty: !isLoading && !data?.length, - refetchUsers: () => mutate(url), - }), - [data, error, isLoading, isValidating] +export function useGetAllUsers(shouldFetch = false) { + const { data, error, isLoading } = useSWR( + shouldFetch ? endpoints.chatbot.user.list : null, + fetcherchatbot, + swrOptions ); - - return memoizedValue; + return { + users: data || [], + usersLoading: isLoading, + usersError: error, + }; } // ---------------------------------------------------------------------- @@ -122,4 +117,38 @@ export function useGetAllConversation() { ); return memoizedValue; -} \ No newline at end of file +} + +// ---------------------------------------------------------------------- +export function useConversations(userIdToFetch:string) { + const url = userIdToFetch + ? endpoints.chatbot.chat.conversations(userIdToFetch) + : endpoints.chatbot.chat.all; + const { data, isLoading, error, mutate } = useSWR(url, fetcherchatbot, swrOptions); + + return { + conversations: data?.conversations || [], + conversationsLoading: isLoading, + refetchConversations: mutate, + }; +} +// ---------------------------------------------------------------------- +export const getCurrentChatUser = async (chatBotToken: string) => { + try { + if (!chatBotToken || !isValidToken(chatBotToken)) { + console.error('Invalid token'); + return null; + } + + const res = await axios.get(endpoints.chatbot.auth.currentUser, { + headers: { + Authorization: `Bearer ${chatBotToken}` + } + }); + + return res.data; + } catch (error) { + console.error('Error getting current chatbot user:', error); + return null; + } +}; diff --git a/src/shared/api/server.ts b/src/shared/api/server.ts index d1c79726..16b40d62 100644 --- a/src/shared/api/server.ts +++ b/src/shared/api/server.ts @@ -2,8 +2,6 @@ import { GATEWAY_API_URL } from "@/config-global"; import { getRequestAuthorization } from "@/utils/encryption"; import axios, { AxiosRequestConfig, CancelTokenSource } from "axios"; - - export const axiosInstance = axios.create({ baseURL: GATEWAY_API_URL }); @@ -31,6 +29,30 @@ axiosInstance.interceptors.response.use( export default axiosInstance; +// -----------------------------CHAT-BOT----------------------------------------- +export const chatbotAxiosInstance = axios.create({ + baseURL: GATEWAY_API_URL, +}); + +chatbotAxiosInstance.interceptors.request.use( + (config) => { + const chatBotToken = localStorage.getItem("chatbotToken"); + if (chatBotToken) { + config.headers.Authorization = `Bearer ${chatBotToken}`; + } + return config; + }, + (error) => Promise.reject(error) +); + +chatbotAxiosInstance.interceptors.response.use( + (res) => res, + (error) => + Promise.reject( + (error.response && error.response.data) || "Something went wrong" + ) +); + export const fetcher = async (args: string | [string, AxiosRequestConfig]) => { const [url, config] = Array.isArray(args) ? args : [args]; const res = await axiosInstance.get(url, { ...config }); @@ -58,7 +80,7 @@ export const fetcherchatbot = async ( if (method === 'post' || method === 'put' || method === 'patch') { axiosConfig.data = data; } - const res = await axiosInstance(url, axiosConfig); + const res = await chatbotAxiosInstance(url, axiosConfig); return res.data; } catch (error) { @@ -282,30 +304,30 @@ export const endpoints = { statusCount : 'api/orders/status-count', }, user: { - login: "/api/user/login/backoffice", - searchAndFetchRandomClients :(searchKey: string)=> "/api/user/searchAndFetchRandomClients?searchKey="+searchKey, - register: "api/user/register", - add: "api/user/addUser", - getAll: "api/user/", - addAdresss:"/api/user/address/addAddress", - updateAdress:"/api/user/address/updateAddress", - getUserById: (id: number) => `api/user/${id}`, - findUserById: (id: number) => `api/user/findUserById/${id}`, - getByEmail: (email: string) => `api/user/getByEmail/${email}`, - changingLogingActivity: "api/user/changeLoginActivity", - updateUser: `api/user/updateUser`, - updateClient: `api/user/updateClient`, - changeUserStatus: "api/user/changeUserStatus", - updateUserPassword: "api/user/updatePassword", - getAllRoles: "api/user/roles/", - addRole: "api/user/roles/addRole", - getClientsByIDs: (clientsIDs: number[]) => `api/user/retrieve-clients-by-ids?clientsIDs=${clientsIDs.join(',')}`, - getUsersByIDs: (usersIDs: number[]) => `api/user/retrieve-users-by-ids?usersIDs=${usersIDs.join(',')}`, - export: '/api/user/export', - searchUsers: '/api/user/filter', - statusCount : 'api/user/status-counts', - getClients : 'api/user/client/search', - clientStatusCount : 'api/user/clients/status-count', + login: "/api/sync/user/login/backoffice", + searchAndFetchRandomClients :(searchKey: string)=> "/api/sync/user/searchAndFetchRandomClients?searchKey="+searchKey, + register: "api/sync/user/register", + add: "api/sync/user/addUser", + getAll: "api/sync/user/", + addAdresss:"/api/sync/user/address/addAddress", + updateAdress:"/api/sync/user/address/updateAddress", + getUserById: (id: number) => `api/sync/user/${id}`, + findUserById: (id: number) => `api/sync/user/findUserById/${id}`, + getByEmail: (email: string) => `api/sync/user/getByEmail/${email}`, + changingLogingActivity: "api/sync/user/changeLoginActivity", + updateUser: `api/sync/user/updateUser`, + updateClient: `api/sync/user/updateClient`, + changeUserStatus: "api/sync/user/changeUserStatus", + updateUserPassword: "api/sync/user/updatePassword", + getAllRoles: "api/sync/user/roles/", + addRole: "api/sync/user/roles/addRole", + getClientsByIDs: (clientsIDs: number[]) => `api/sync/user/retrieve-clients-by-ids?clientsIDs=${clientsIDs.join(',')}`, + getUsersByIDs: (usersIDs: number[]) => `api/sync/user/retrieve-users-by-ids?usersIDs=${usersIDs.join(',')}`, + export: '/api/sync/user/export', + searchUsers: '/api/sync/user/filter', + statusCount : 'api/sync/user/status-counts', + getClients : 'api/sync/user/client/search', + clientStatusCount : 'api/sync/user/clients/status-count', }, stock_management: { products: { @@ -438,13 +460,13 @@ export const endpoints = { statusCount : '/api/stock/supplier-order/status', }, salleSession: { - getClients: `/api/user/clients`, - updateClient: (id: string) => `/api/user/clients/${id}`, - banClient: (clientId: string) => `/api/user/clients/ban/${clientId}`, - unbanClient: (clientId: string) => `/api/user/clients/unban/${clientId}`, - getUnregistredClients: `/api/user/unregistered-client/`, - addUnregistredClient: `/api/user/unregistered-client/add`, - associateAndDelete: "/api/user/unregistered-client/associate-unregistered-client", + getClients: `/api/sync/user/clients`, + updateClient: (id: string) => `/api/sync/user/clients/${id}`, + banClient: (clientId: string) => `/api/sync/user/clients/ban/${clientId}`, + unbanClient: (clientId: string) => `/api/sync/user/clients/unban/${clientId}`, + getUnregistredClients: `/api/sync/user/unregistered-client/`, + addUnregistredClient: `/api/sync/user/unregistered-client/add`, + associateAndDelete: "/api/sync/user/unregistered-client/associate-unregistered-client", addSaleSession: `/api/cart/add-saleSession`, getSessions: `/api/cart/saleSessions`, dateRangeSessions: `/api/cart/rangeSaleSessions`, @@ -456,7 +478,7 @@ export const endpoints = { deleteOrder: (orderId: string) => `/api/cart/ordersSession/delete-order/${orderId}`, getSaleSessionById: (saleSessionId: string) => `/api/cart/SaleSession/${saleSessionId}`, modifieExpirationDate: (saleSessionId: string) => `api/cart/${saleSessionId}/expiration-date`, - deleteUnregisteredClientById: `/api/user/unregistered-client/deleteUnregisteredClient`, + deleteUnregisteredClientById: `/api/sync/user/unregistered-client/deleteUnregisteredClient`, updateDetails: (saleSessionId: string) => `api/cart/${saleSessionId}/details`, deleteSaleSessionById: (id: number) => `/api/cart/deleteSession/${id}`, restoreSaleSession: (id: number) => `/api/cart/restock/${id}`, @@ -464,21 +486,21 @@ export const endpoints = { getTypeCount:"/api/cart/count-type", getDeletedSeleSessionPage: "/api/cart/deleted/page", getDeletedTypeCount:"/api/cart/deleted/count-type", - getUnregisteredClientPage:"/api/user/unregistered-client/page" + getUnregisteredClientPage:"/api/sync/user/unregistered-client/page" }, supplier: { - getAllSupplier: "/api/user/supplier", - getSupplierDetails: (id: string) => `/api/user/supplier/${id}`, - addSupplier: "/api/user/supplier/add", - deleteSupplier: (id: number) => `/api/user/supplier/delete/${id}`, - updateSupplier: "/api/user/supplier/update", - archiveSupplier: (id: number, isArchived: boolean) => `/api/user/supplier/${id}/${isArchived}`, - getArchivedSuppliers: "/api/user/supplier/archived", - getNotArchivedSuppliers: "/api/user/supplier/notArchived", + getAllSupplier: "/api/sync/user/supplier", + getSupplierDetails: (id: string) => `/api/sync/user/supplier/${id}`, + addSupplier: "/api/sync/user/supplier/add", + deleteSupplier: (id: number) => `/api/sync/user/supplier/delete/${id}`, + updateSupplier: "/api/sync/user/supplier/update", + archiveSupplier: (id: number, isArchived: boolean) => `/api/sync/user/supplier/${id}/${isArchived}`, + getArchivedSuppliers: "/api/sync/user/supplier/archived", + getNotArchivedSuppliers: "/api/sync/user/supplier/notArchived", }, driver: { - getAllDrivers: "api/user/driver", - getDriverById: (id: string) => `api/user/driver/${id}`, + getAllDrivers: "api/sync/user/driver", + getDriverById: (id: string) => `api/sync/user/driver/${id}`, }, saleOrder: { getAllSaleOrders: "/api/stock/sale-orders", @@ -537,6 +559,9 @@ export const endpoints = { } }, chatbot : { + auth: { + currentUser: '/api/chatbot/users/current', + }, faq: { list: '/api/chatbot/faq/all', add: '/api/chatbot/faq/create', diff --git a/src/shared/layouts/common/account-popover.tsx b/src/shared/layouts/common/account-popover.tsx index ec9d9b02..03472bdd 100644 --- a/src/shared/layouts/common/account-popover.tsx +++ b/src/shared/layouts/common/account-popover.tsx @@ -88,6 +88,7 @@ export default function AccountPopover() { if (result.status === 200) { localStorage.removeItem("token"); + localStorage.removeItem("chatbotToken"); localStorage.removeItem("refreshToken"); localStorage.removeItem("expirationDurationInSec"); localStorage.removeItem("userId"); diff --git a/src/shared/layouts/dashboard/config-navigation.tsx b/src/shared/layouts/dashboard/config-navigation.tsx index 75bd7f95..5d97c23b 100644 --- a/src/shared/layouts/dashboard/config-navigation.tsx +++ b/src/shared/layouts/dashboard/config-navigation.tsx @@ -352,10 +352,6 @@ export function useNavData() { { subheader: 'Chatbot Configuration', items: [ - /** - * Manage FAQ - */ - { title: 'Manage FAQ', path: paths.dashboard.chatbot.faq.root, @@ -365,9 +361,6 @@ export function useNavData() { { title: 'Nouvelle FAQ', path: paths.dashboard.chatbot.faq.new }, ], }, - /** - * Manage Help - */ { title: 'Centre Aide', path: paths.dashboard.chatbot.help.root, @@ -377,9 +370,6 @@ export function useNavData() { { title: 'Nouvelle Collection', path: paths.dashboard.chatbot.help.new }, ], }, - /** - * Manage OpenAi API - */ { title: 'OpenAi API', path: paths.dashboard.chatbot.openai.list('1'), @@ -389,9 +379,6 @@ export function useNavData() { { title: 'Configuration API', path: paths.dashboard.chatbot.openai.list('1') }, ], }, - /** - * Manage Config Support Agent - */ { title: 'Gestion Des Tickets', path: paths.dashboard.chatbot.ticket.root, @@ -661,12 +648,24 @@ export function useNavData() { path: paths.dashboard.admin.media.root, icon: ICONS.file, }, + { + subheader: 'Chatbot Configuration', + items: [ + { + title: 'Gestion Des Tickets', + path: paths.dashboard.chatbot.ticket.root, + icon: ICONS.mail, + children: [ + { title: 'Liste Des Tickets', path: paths.dashboard.chatbot.ticket.root }, + { title : 'Voir Les Conversations', path: paths.dashboard.chatbot.ticket.conversations }, + ], + }, + ], + }, ], }, ]; - } else if ( - userInfo?.realm_access?.roles?.includes("GESTIONNAIRE_DE_PRECO") - ) { + } else if (userInfo?.realm_access?.roles?.includes("GESTIONNAIRE_DE_PRECO")) { return [ { subheader: "MyDressin", @@ -814,12 +813,25 @@ export function useNavData() { }, ], }, - { title: "Médiathèque", path: paths.dashboard.admin.media.root, icon: ICONS.file, }, + { + subheader: 'Chatbot Configuration', + items: [ + { + title: 'Gestion Des Tickets', + path: paths.dashboard.chatbot.ticket.root, + icon: ICONS.mail, + children: [ + { title: 'Liste Des Tickets', path: paths.dashboard.chatbot.ticket.root }, + { title : 'Voir Les Conversations', path: paths.dashboard.chatbot.ticket.conversations }, + ], + }, + ], + }, ], }, ]; @@ -967,6 +979,20 @@ export function useNavData() { path: paths.dashboard.admin.media.root, icon: ICONS.file, }, + { + subheader: 'Chatbot Configuration', + items: [ + { + title: 'Gestion Des Tickets', + path: paths.dashboard.chatbot.ticket.root, + icon: ICONS.mail, + children: [ + { title: 'Liste Des Tickets', path: paths.dashboard.chatbot.ticket.root }, + { title : 'Voir Les Conversations', path: paths.dashboard.chatbot.ticket.conversations }, + ], + }, + ], + }, ], }, ]; @@ -998,9 +1024,6 @@ export function useNavData() { { subheader: "SUPPORT", items: [ - /** - * Manage Support Agent - */ { title: 'Support Agent', path: paths.dashboard.chatbot.support.root, diff --git a/src/shared/sections/auth/jwt/jwt-login-view.tsx b/src/shared/sections/auth/jwt/jwt-login-view.tsx index ca02fb42..0973a13c 100644 --- a/src/shared/sections/auth/jwt/jwt-login-view.tsx +++ b/src/shared/sections/auth/jwt/jwt-login-view.tsx @@ -15,7 +15,17 @@ import { useRouter, useSearchParams } from "@/hooks"; import { useBoolean } from "@/hooks"; import { useAuthContext } from "@/hooks"; -import { PATH_AFTER_LOGIN, PATH_AFTER_LOGIN_DRIVER, PATH_AFTER_LOGIN_GESTIONNAIRE_DE_PRECO, PATH_AFTER_LOGIN_GESTIONNAIRE_RETOUR, PATH_AFTER_LOGIN_LIVE_MANAGER, PATH_AFTER_LOGIN_SALES_MANAGER, PATH_AFTER_LOGIN_STOCK_MANAGER } from "@/config-global"; +import { + PATH_AFTER_LOGIN, + PATH_AFTER_LOGIN_DRIVER, + PATH_AFTER_LOGIN_GESTIONNAIRE_DE_PRECO, + PATH_AFTER_LOGIN_GESTIONNAIRE_RETOUR, + PATH_AFTER_LOGIN_LIVE_MANAGER, + PATH_AFTER_LOGIN_SALES_MANAGER, + PATH_AFTER_LOGIN_STOCK_MANAGER , + PATH_AFTER_LOGIN_LEADER, + PATH_AFTER_LOGIN_SUPPORT +} from "@/config-global"; import Iconify from "@/shared/components/iconify"; import FormProvider, { RHFTextField } from "@/shared/components/hook-form"; @@ -63,10 +73,11 @@ export default function JwtLoginView() { const response = await loginUser(data); if (response.status === 200) { - const { token, refreshToken, expirationDurationInSec } = response.data; + const { token, refreshToken, expirationDurationInSec } = response.data.mainResponse; // Stocker les informations dans localStorage localStorage.setItem("token", token); + localStorage.setItem("chatbotToken", response.data.chatbotResponse.token); localStorage.setItem("refreshToken", refreshToken); localStorage.setItem("expirationDurationInSec", expirationDurationInSec.toString()); localStorage.setItem("email", data.username); @@ -88,9 +99,13 @@ export default function JwtLoginView() { router.push(returnTo || PATH_AFTER_LOGIN_GESTIONNAIRE_DE_PRECO); } else if (userRoles.includes("GESTIONNAIRE_RETOUR")) { router.push(returnTo || PATH_AFTER_LOGIN_GESTIONNAIRE_RETOUR); + } else if (userRoles.includes("ADMIN")) { + router.push(returnTo || PATH_AFTER_LOGIN); + } else if (userRoles.includes("LEADER")) { + router.push(returnTo || PATH_AFTER_LOGIN_LEADER); + } else if (userRoles.includes("SUPPORT")) { + router.push(returnTo || PATH_AFTER_LOGIN_SUPPORT); } - else if (userRoles.includes("ADMIN")) { - router.push(returnTo || PATH_AFTER_LOGIN);} else { router.push(returnTo || PATH_AFTER_LOGIN); } diff --git a/src/shared/sections/chat-bot/faq/view/faq-list-view.tsx b/src/shared/sections/chat-bot/faq/view/faq-list-view.tsx index 8c9cb9e4..41ec15f1 100644 --- a/src/shared/sections/chat-bot/faq/view/faq-list-view.tsx +++ b/src/shared/sections/chat-bot/faq/view/faq-list-view.tsx @@ -45,13 +45,6 @@ import { // ---------------------------------------------------------------------- -const PUBLISH_OPTIONS = [ - { value: 'BILLING', label: 'Billing' }, - { value: 'GENERAL', label: 'General' }, - { value: 'TECHNICAL', label: 'Technical' }, - { value: 'OTHER', label: 'Other' }, -]; - const HIDE_COLUMNS = { category: false }; const HIDE_COLUMNS_TOGGLABLE = ['category', 'actions']; @@ -202,7 +195,9 @@ export function FAQListView() { <> - <Container sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}> + <Container + maxWidth="xl" + sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}> <CustomBreadcrumbs heading="Liste FAQ" links={[{ name: 'Tableau de bord' }, { name: 'Liste FAQ' }]} diff --git a/src/shared/sections/chat-bot/help/help-item-horizontal.tsx b/src/shared/sections/chat-bot/help/help-item-horizontal.tsx index 3d8bfe63..ee495f3c 100644 --- a/src/shared/sections/chat-bot/help/help-item-horizontal.tsx +++ b/src/shared/sections/chat-bot/help/help-item-horizontal.tsx @@ -91,7 +91,7 @@ export function CollectionItemHorizontal({ collection }: Props) { <Stack spacing={1} flexGrow={1}> <Link component={RouterLink} - href={paths.dashboard.chatbot.help.details(name)} + href={paths.dashboard.chatbot.help.details(id)} color="inherit" variant="subtitle2" sx={{ ...maxLine({ line: 2 }) }} diff --git a/src/shared/sections/chat-bot/help/view/help-list-view.tsx b/src/shared/sections/chat-bot/help/view/help-list-view.tsx index 4573623c..d9cdf841 100644 --- a/src/shared/sections/chat-bot/help/view/help-list-view.tsx +++ b/src/shared/sections/chat-bot/help/view/help-list-view.tsx @@ -25,7 +25,7 @@ export function CollectionListView() { ); return ( - <Container> + <Container maxWidth={false}> <CustomBreadcrumbs heading="Liste Collection" links={[ diff --git a/src/shared/sections/chat-bot/newchat/chat-empty-state.tsx b/src/shared/sections/chat-bot/newchat/chat-empty-state.tsx index ceff1e1b..3b040a30 100644 --- a/src/shared/sections/chat-bot/newchat/chat-empty-state.tsx +++ b/src/shared/sections/chat-bot/newchat/chat-empty-state.tsx @@ -50,13 +50,6 @@ export function ChatEmptyState( }} {...other} > - <Box - component="img" - alt="empty content" - src={imgUrl ?? `/assets/icons/empty/ic-content.svg`} - sx={{ width: 1, maxWidth: 160, ...slotProps?.img }} - /> - {title ? ( <Typography variant="h6" diff --git a/src/shared/sections/chat-bot/newchat/chat-layout.tsx b/src/shared/sections/chat-bot/newchat/chat-layout.tsx index eee48da0..edbcaba8 100644 --- a/src/shared/sections/chat-bot/newchat/chat-layout.tsx +++ b/src/shared/sections/chat-bot/newchat/chat-layout.tsx @@ -15,8 +15,13 @@ type Props = StackProps & { }; export function ChatLayout({ slots, sx, ...other }: Props) { - const renderNav = ( - <Box display="flex" flexDirection="column"> + const renderNav = ( + <Box + sx={{ + borderRight: (theme) => `solid 1px ${theme.palette.divider}`, + }} + display="flex" flexDirection="column" + > {slots.nav} </Box> ); @@ -40,7 +45,7 @@ export function ChatLayout({ slots, sx, ...other }: Props) { const renderMain = <Stack sx={{ flex: '1 1 auto', minWidth: 0 }}>{slots.main}</Stack>; - const renderDetails = <Stack sx={{ minHeight: 0 }}>{slots.details}</Stack>; + const renderDetails = <Stack sx={{ width: 280, flexShrink: 0 }}>{slots.details}</Stack>; return ( <Stack direction="row" sx={sx} {...other}> diff --git a/src/shared/sections/chat-bot/newchat/chat-list.tsx b/src/shared/sections/chat-bot/newchat/chat-list.tsx index 912b1c1b..6b4ffc5e 100644 --- a/src/shared/sections/chat-bot/newchat/chat-list.tsx +++ b/src/shared/sections/chat-bot/newchat/chat-list.tsx @@ -22,7 +22,7 @@ import { fToNow } from "@/utils/format-time"; import Iconify from "@/shared/components/iconify"; import Scrollbar from "@/shared/components/scrollbar"; import { useCollapseNav , useResponsive } from "@/hooks"; -import { useGetUserById } from "@/shared/api/user"; +import { useGetUserById } from "@/shared/api/ticket"; type Props = { @@ -49,8 +49,7 @@ function ChatConversationItem({ selectedConversationId, onClickConversation, }: ChatConversationItemProps) { - const { userData } = useGetUserById(chat.user_id); - const user = userData; + const { user } = useGetUserById(chat.user_id); const displayName = user ? `${user.firstName} ${user.lastName}` : chat.title; @@ -137,53 +136,69 @@ export function ChatList({ }, [mdUp, onCloseMobile, onCollapseDesktop]); return ( - <Stack - sx={{ - minHeight: 0, - flex: "1 1 auto", - display: { xs: "none", md: "flex" }, - borderRight: `solid 1px ${theme.palette.divider}`, - transition: theme.transitions.create(["width"], { - duration: theme.transitions.duration.shorter, - }), - ...(collapseDesktop && { width: NAV_COLLAPSE_WIDTH }), - }} - > - <Stack direction="row" alignItems="center" justifyContent="center" sx={{ p: 2.5, pb: 0 }}> - {!collapseDesktop && ( - <> - <ChatNavAccount - user={user} - users={users} - ticketFilter={ticketFilter} - onChangeTicketFilter={onChangeTicketFilter} - /> - <Box sx={{ flexGrow: 1 }} /> - </> - )} - <IconButton onClick={handleToggleNav}> - <Iconify - icon={collapseDesktop ? "eva:arrow-ios-forward-fill" : "eva:arrow-ios-back-fill"} - /> - </IconButton> - </Stack> + <Stack + sx={{ + minHeight: 0, + flex: 1, + width: collapseDesktop ? NAV_COLLAPSE_WIDTH : 350, + display: { xs: "none", md: "flex" }, + flexDirection: "column", + transition: theme.transitions.create(["width"], { + duration: theme.transitions.duration.shorter, + }), + ...(collapseDesktop && { width: NAV_COLLAPSE_WIDTH }), + position: 'relative', // Add this to make positioning more predictable + }} + > + <Stack direction="row" alignItems="center" justifyContent="center" sx={{ p: 2.5, pb: 0, flexShrink: 0 }}> {!collapseDesktop && ( - <TextField - fullWidth - value={searchContacts.query} - onChange={(event) => handleSearchContacts(event.target.value)} - placeholder="Rechercher un Contact ..." - InputProps={{ - startAdornment: ( - <InputAdornment position="start"> - <Iconify icon="eva:search-fill" sx={{ color: "text.disabled" }} /> - </InputAdornment> - ), - }} - sx={{ mt: 2.5, p: 2.5, pt: 0 }} - /> + <> + <ChatNavAccount + user={user} + users={users} + ticketFilter={ticketFilter} + onChangeTicketFilter={onChangeTicketFilter} + /> + <Box sx={{ flexGrow: 1 }} /> + </> )} - <Scrollbar sx={{ pb: 1, overflowX: "hidden" }}> + <IconButton onClick={handleToggleNav}> + <Iconify + icon={collapseDesktop ? "eva:arrow-ios-forward-fill" : "eva:arrow-ios-back-fill"} + /> + </IconButton> + </Stack> + + {!collapseDesktop && ( + <TextField + fullWidth + value={searchContacts.query} + onChange={(event) => handleSearchContacts(event.target.value)} + placeholder="Rechercher un Contact ..." + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + <Iconify icon="eva:search-fill" sx={{ color: "text.disabled" }} /> + </InputAdornment> + ), + }} + sx={{ mt: 2.5, p: 2.5, pt: 0, flexShrink: 0 }} + /> + )} + + {/* Modified Scrollbar with absolute position to prevent double scrollbars */} + <Box sx={{ + flex: 1, + position: 'relative', // This is important + overflow: 'hidden', // Hide any overflow at this level + }}> + <Scrollbar + sx={{ + position: 'absolute', + inset: 0, // Take up all the space + overflowX: "hidden" // Disable horizontal scrolling + }} + > <List> {filteredConversations.map((chat) => ( <ChatConversationItem @@ -195,6 +210,7 @@ export function ChatList({ ))} </List> </Scrollbar> - </Stack> - ); + </Box> + </Stack> +); } \ No newline at end of file diff --git a/src/shared/sections/chat-bot/newchat/chat-message-input.tsx b/src/shared/sections/chat-bot/newchat/chat-message-input.tsx index 5b9a3576..42d9f9bd 100644 --- a/src/shared/sections/chat-bot/newchat/chat-message-input.tsx +++ b/src/shared/sections/chat-bot/newchat/chat-message-input.tsx @@ -5,19 +5,26 @@ import Box from '@mui/material/Box'; import Iconify from "@/components/iconify"; import { useGetTicketByConversation } from "@/shared/api/chat-bot"; import { IMessageSocketItem } from "@/shared/types/chat-bot"; +import { is } from "date-fns/locale"; type Props = { conversationId: string; onSendMessage: (message: IMessageSocketItem) => void; userid: string; + ticketFilter?: string; }; -export function ChatMessageInput({ conversationId, onSendMessage ,userid}: Props) { +export function ChatMessageInput({ conversationId, onSendMessage, userid, ticketFilter }: Props) { const [message, setMessage] = useState(""); const { ticket } = useGetTicketByConversation(conversationId); + const isDisabled = ticketFilter !== "me"; + const placeholderText = isDisabled + ? "Vous ne faites pas partie de cette conversation" + : "Commencer Votre Conversation ..."; + const sendMessage = () => { - if (message.trim()) { + if (message.trim() && !isDisabled) { onSendMessage({ ticketId: ticket?.id || "", senderId: userid, @@ -49,8 +56,9 @@ export function ChatMessageInput({ conversationId, onSendMessage ,userid}: Props <InputBase name="chat-message" id="chat-message-input" - placeholder="Commencer Votre Conversation ..." + placeholder={placeholderText} value={message} + disabled={isDisabled} onChange={(e) => setMessage(e.target.value)} onKeyUp={handleSendMessage} sx={{ @@ -61,9 +69,13 @@ export function ChatMessageInput({ conversationId, onSendMessage ,userid}: Props flexGrow: 1, }} /> - <IconButton onClick={handleSendMessageClick}> + {!isDisabled ? ( + <IconButton onClick={handleSendMessageClick}> <Iconify icon="mdi:send" /> </IconButton> + ) : ( + null + )} </Box> ); } diff --git a/src/shared/sections/chat-bot/newchat/chat-room-single.tsx b/src/shared/sections/chat-bot/newchat/chat-room-single.tsx index 8764db41..18e14dcf 100644 --- a/src/shared/sections/chat-bot/newchat/chat-room-single.tsx +++ b/src/shared/sections/chat-bot/newchat/chat-room-single.tsx @@ -6,17 +6,18 @@ import Typography from '@mui/material/Typography'; import { useBoolean } from '@/hooks'; import Iconify from '@/components/iconify'; import { CollapseButton } from './styles'; +import { useGetUserById } from '@/shared/api/ticket'; // ---------------------------------------------------------------------- type Props = { - clientId: string; - user: any; + support_id: string; }; -export function ChatRoomSingle({ clientId , user }: Props) { +export function ChatRoomSingle({ support_id }: Props) { const collapse = useBoolean(true); + const { user } = useGetUserById(support_id); const renderInfo = ( <Stack alignItems="center" sx={{ py: 5 }}> diff --git a/src/shared/sections/chat-bot/newchat/chat-room.tsx b/src/shared/sections/chat-bot/newchat/chat-room.tsx index fe3a1200..c4103d73 100644 --- a/src/shared/sections/chat-bot/newchat/chat-room.tsx +++ b/src/shared/sections/chat-bot/newchat/chat-room.tsx @@ -10,6 +10,7 @@ import { CircularProgress } from '@mui/material'; import { useCollapseNav } from '@/hooks'; import { useGetConversation } from '@/shared/api/chat-bot'; +import { useGetTicketByConversation } from '@/shared/api/chat-bot'; // ---------------------------------------------------------------------- @@ -25,6 +26,7 @@ type Props = { export function ChatRoom({ collapseNav, conversationId , user }: Props) { const theme = useTheme(); const { conversation, conversationLoading } = useGetConversation(conversationId); + const { ticket } = useGetTicketByConversation(conversationId); const { collapseDesktop, openMobile, onCloseMobile } = collapseNav; const renderContent = conversationLoading ? ( @@ -39,7 +41,7 @@ export function ChatRoom({ collapseNav, conversationId , user }: Props) { </Stack> ) : ( <Scrollbar> - <ChatRoomSingle clientId={conversation?.user_id} user={user} /> + <ChatRoomSingle support_id={ticket?.support_id || ""} /> </Scrollbar> ); @@ -49,16 +51,14 @@ export function ChatRoom({ collapseNav, conversationId , user }: Props) { sx={{ minHeight: 0, flex: '1 1 auto', - width: NAV_WIDTH, display: { xs: 'none', lg: 'flex' }, borderLeft: `solid 1px ${theme.palette.divider}`, transition: theme.transitions.create(['width'], { duration: theme.transitions.duration.shorter, }), - ...(collapseDesktop && { width: 0 }), }} > - {!collapseDesktop && renderContent} + {renderContent} </Stack> <Drawer diff --git a/src/shared/sections/chat-bot/newchat/view/chat-view.tsx b/src/shared/sections/chat-bot/newchat/view/chat-view.tsx index fa5b1075..577a9872 100644 --- a/src/shared/sections/chat-bot/newchat/view/chat-view.tsx +++ b/src/shared/sections/chat-bot/newchat/view/chat-view.tsx @@ -1,48 +1,69 @@ -"use client"; -import React, { useState , useCallback , useEffect} from "react"; -import { ChatList } from "../chat-list"; -import { ChatWindow } from "../chat-window"; -import { ChatHeader } from "../chat-header"; -import { ChatMessageInput } from "../chat-message-input"; -import { CircularProgress, Stack } from "@mui/material"; -import Container from "@mui/material/Container"; -import { ChatLayout } from "../chat-layout"; -import { ChatRoom } from "../chat-room"; +'use client'; -import { useWebSocket } from "@/hooks/use-web-socket"; -import CustomBreadcrumbs from "@/shared/components/custom-breadcrumbs"; -import { useAuthContext } from "@/hooks"; -import { useCollapseNav } from "@/hooks"; -import { - useGetAllUsers , - useGetAllConversation , - useGetConversations -} from "@/shared/api/chat-bot"; +import React, { useState, useEffect, useCallback } from 'react'; +import Container from '@mui/material/Container'; +import { CircularProgress, Stack } from '@mui/material'; + +import CustomBreadcrumbs from '@/shared/components/custom-breadcrumbs'; +import { useCollapseNav } from '@/hooks'; +import { useWebSocket } from '@/hooks/use-web-socket'; +import { + useGetAllUsers, + getCurrentChatUser, + useConversations, // Import the new custom Hook +} from '@/shared/api/chat-bot'; +import { ChatList } from '../chat-list'; +import { ChatWindow } from '../chat-window'; +import { ChatHeader } from '../chat-header'; +import { ChatMessageInput } from '../chat-message-input'; +import { ChatLayout } from '../chat-layout'; +import { ChatRoom } from '../chat-room'; +import { IMessageSocketItem } from '@/shared/types/chat-bot'; +import { ChatUser } from '@/shared/types/user'; +import { ChatEmptyState } from '../chat-empty-state'; export function ChatView() { - const user = useAuthContext().user; - const userId = user?.id; + const [userData, setUserData] = useState<ChatUser | null>(null); + + useEffect(() => { + async function fetchUser() { + const token = localStorage.getItem('chatbotToken'); + if (token) { + const data = await getCurrentChatUser(token); + setUserData(data); + } + } + fetchUser(); + }, []); - const { users } = (user?.role === "ADMIN" || user?.role === "LEADER") ? useGetAllUsers(): { users: [] }; + const userId = userData?.id; + const isAdminOrLeader = userData?.role === 'ADMIN' || userData?.role === 'LEADER'; + + const { users } = useGetAllUsers(isAdminOrLeader); const [selectedConversationId, setSelectedConversationId] = useState<string | null>(null); - const [ticketFilter, setTicketFilter] = useState<string>("me"); + const [ticketFilter, setTicketFilter] = useState('me'); + const [previousTicketFilter, setPreviousTicketFilter] = useState('me'); const userIdToFetch = - ticketFilter === "all" ? null : ticketFilter === "me" ? userId : ticketFilter; + ticketFilter === 'all' ? null : ticketFilter === 'me' ? userId : ticketFilter; - // Get the refetch function from your hook - const { - conversations, - conversationsLoading, - refetchConversations - } = userIdToFetch ? useGetConversations(userIdToFetch) : useGetAllConversation(); + // Use the custom Hook unconditionally + const { conversations, conversationsLoading, refetchConversations } = useConversations(userIdToFetch); + + // Reset selected conversation when ticket filter changes + useEffect(() => { + if (ticketFilter !== previousTicketFilter) { + setSelectedConversationId(null); + setPreviousTicketFilter(ticketFilter); + } + }, [ticketFilter, previousTicketFilter]); - const sortedConversations = conversations?.sort((a: any, b: any) => { - const aLatestTimestamp = a.messages && a.messages.length + const sortedConversations = conversations?.sort((a:any, b:any) => { + const aLatestTimestamp = a.messages?.length ? new Date(a.messages[a.messages.length - 1].timestamp).getTime() : new Date(a.createdAt).getTime(); - const bLatestTimestamp = b.messages && b.messages.length + const bLatestTimestamp = b.messages?.length ? new Date(b.messages[b.messages.length - 1].timestamp).getTime() : new Date(b.createdAt).getTime(); return bLatestTimestamp - aLatestTimestamp; @@ -52,33 +73,30 @@ export function ChatView() { const conversationsNav = useCollapseNav(); useEffect(() => { - if (messages && messages.length > 0) { - refetchConversations(); + if (messages?.length > 0) { + refetchConversations(); } }, [messages, refetchConversations]); - const handleSendMessage = useCallback(async (message: any) => { - sendMessage(message); - }, [sendMessage]); + const handleSendMessage = useCallback( + async (message:IMessageSocketItem) => { + sendMessage(message); + }, + [sendMessage] + ); return ( <Container - maxWidth="xl" - sx={{ display: "flex", flex: "1 1 auto", flexDirection: "column" }} + maxWidth={false} + sx={{ display: 'flex', flex: '1 1 auto', flexDirection: 'column' }} > - <CustomBreadcrumbs - heading="Voir Les Conversations" - links={[{ name: "Tableau de bord" }, { name: "Voir Les Conversations" }]} - sx={{ mb: { xs: 5, md: 5 } }} - /> - <ChatLayout sx={{ minHeight: 0, - flex: "1 1 0", + flex: '1 1 0', borderRadius: 1.3, - position: "relative", - bgcolor: "background.paper", + position: 'relative', + bgcolor: 'background.paper', boxShadow: (theme) => theme.customShadows.card, }} slots={{ @@ -88,11 +106,11 @@ export function ChatView() { <CircularProgress /> ) : ( <ChatList - user={user} + user={userData} users={users} conversations={sortedConversations || []} - onClickConversation={setSelectedConversationId} - selectedConversationId={selectedConversationId || ""} + onClickConversation={(id: string) => setSelectedConversationId(id)} + selectedConversationId={selectedConversationId || ''} collapseNav={conversationsNav} ticketFilter={ticketFilter} onChangeTicketFilter={setTicketFilter} @@ -100,31 +118,40 @@ export function ChatView() { )} </> ), - header: ( - <ChatHeader conversationId={selectedConversationId || ""} /> - ), + header: selectedConversationId ? ( + <ChatHeader conversationId={selectedConversationId} /> + ) : null, main: ( - <Stack direction="column" spacing={2} sx={{ height: "100%" }}> - <ChatWindow - conversationId={selectedConversationId || ""} - socketMessages={messages} - /> - {selectedConversationId && ( - <ChatMessageInput - conversationId={selectedConversationId} - onSendMessage={handleSendMessage} - userid={userId} + <Stack direction="column" spacing={2} sx={{ height: '100%' }}> + {selectedConversationId ? ( + <> + <ChatWindow + conversationId={selectedConversationId} + socketMessages={messages} + key={`${selectedConversationId}-${ticketFilter}`} + /> + <ChatMessageInput + conversationId={selectedConversationId} + onSendMessage={handleSendMessage} + userid={userId} + ticketFilter={ticketFilter} + /> + </> + ) : ( + <ChatEmptyState + title="Bonjour!" + description="Merci de sélectionner une conversation pour commencer à discuter." /> )} </Stack> ), details: selectedConversationId && ( <ChatRoom - user={user} + user={userData} collapseNav={conversationsNav} conversationId={selectedConversationId} /> - ), + ), }} /> </Container> diff --git a/src/shared/sections/chat-bot/support/view/support-list-view.tsx b/src/shared/sections/chat-bot/support/view/support-list-view.tsx index 420d472c..e374cbce 100644 --- a/src/shared/sections/chat-bot/support/view/support-list-view.tsx +++ b/src/shared/sections/chat-bot/support/view/support-list-view.tsx @@ -48,7 +48,8 @@ import { } from '../support-table-row'; import { ITiketItem } from '@/shared/types/ticket'; -import { useAuthContext } from '@/hooks'; +import { ChatUser } from '@/shared/types/user'; +import { getCurrentChatUser } from '@/shared/api/chat-bot'; const HIDE_COLUMNS = { category: false }; @@ -58,7 +59,20 @@ const HIDE_COLUMNS_TOGGLABLE = ['category', 'actions']; export function SupportTicketListView() { const confirmRows = useBoolean(); - const { user } = useAuthContext(); + + const [userData, setUserData] = useState<ChatUser | null>(null); + useEffect(() => { + async function fetchUser() { + const token = localStorage.getItem('chatbotToken'); + if (token) { + const data = await getCurrentChatUser(token); + setUserData(data); + } + } + fetchUser(); + }, []); + + const user = userData || null; const { tickets, ticketsLoading } = useGetTicketBySupportId(user?.id ?? ''); const { updateTicket } = useUpdateTicket(); const router = useRouter(); diff --git a/src/shared/sections/chat-bot/ticket/ticket-table-row.tsx b/src/shared/sections/chat-bot/ticket/ticket-table-row.tsx index d0559e46..05ab857b 100644 --- a/src/shared/sections/chat-bot/ticket/ticket-table-row.tsx +++ b/src/shared/sections/chat-bot/ticket/ticket-table-row.tsx @@ -65,7 +65,16 @@ export function RenderCellStatus({ params }: ParamsProps) { (params.row.ticketstatus === 'REJECTEED' && 'error') || 'default' } - sx={{ minWidth: 220, cursor: 'pointer' }} + sx={{ + width: '100%', + maxWidth: '220px', + cursor: 'pointer', + textAlign: 'center', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + px: 1 + }} onClick={handleOpenMenu} > {categories[params.row.ticketstatus]} @@ -76,9 +85,23 @@ export function RenderCellStatus({ params }: ParamsProps) { onClose={handleCloseMenu} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} transformOrigin={{ vertical: 'top', horizontal: 'left' }} + PaperProps={{ + sx: { + maxWidth: '100%', + width: { xs: '180px', sm: '220px' } + } + }} > {Object.entries(categories).map(([key, value]) => ( - <MenuItem key={key} onClick={() => handleStatusChange(key)}> + <MenuItem + key={key} + onClick={() => handleStatusChange(key)} + sx={{ + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' + }} + > {value} </MenuItem> ))} @@ -130,22 +153,47 @@ export function RenderCellAssignedAt({ params }: ParamsProps) { export function RenderCellTicketTitle({ params }: ParamsProps) { const router = useRouter(); + return ( - <Stack direction="row" alignItems="center" sx={{ py: 2, width: 'auto' }}> + <Stack + direction="row" + alignItems="center" + sx={{ + py: { xs: 1, sm: 2 }, + width: "100%", + flexGrow: 1, + }} + > <ListItemText disableTypography primary={ <Link noWrap color="inherit" - variant="subtitle2" - sx={{ cursor: 'pointer' }} - onClick={() => router.push(paths.dashboard.chatbot.ticket.view(params.row.id))} + onClick={() => + router.push( + paths.dashboard.chatbot.ticket.view(params.row.id) + ) + } + sx={{ + cursor: "pointer", + typography: { xs: "body2", sm: "subtitle2" }, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + display: "block", + maxWidth: { xs: 120, sm: 200, md: 300 }, + }} > {params.row.title} </Link> } - sx={{ display: 'flex', flexDirection: 'column', px: 4 }} + sx={{ + display: "flex", + flexDirection: "column", + px: { xs: 1, sm: 4 }, + width: "100%", + }} /> </Stack> ); diff --git a/src/shared/sections/chat-bot/ticket/view/ticket-history-data-view.tsx b/src/shared/sections/chat-bot/ticket/view/ticket-history-data-view.tsx index 8aaa66b4..7df6f2f7 100644 --- a/src/shared/sections/chat-bot/ticket/view/ticket-history-data-view.tsx +++ b/src/shared/sections/chat-bot/ticket/view/ticket-history-data-view.tsx @@ -1,25 +1,58 @@ "use client"; -import React, { useRef, useState } from "react"; +import React, { useRef, useState, useEffect } from "react"; import { Box, CircularProgress, Typography, Card, CardContent, + CardHeader, Divider, Avatar, Grid, Chip, Menu, - MenuItem + MenuItem, + Paper, + Tooltip, + Badge, + IconButton, + LinearProgress, + useTheme } from "@mui/material"; + +import { + Timeline, + TimelineItem, + TimelineSeparator, + TimelineConnector, + TimelineContent, + TimelineDot, + TimelineOppositeContent +} from '@mui/lab'; + +import { + AccessTime as AccessTimeIcon, + PersonOutline as PersonIcon, + Flag as PriorityIcon, + Category as CategoryIcon, + Update as UpdateIcon, + Create as CreateIcon, + Chat as ChatIcon, + MoreVert as MoreVertIcon, + Email as EmailIcon, + Assignment as AssignmentIcon +} from "@mui/icons-material"; import { ChatEmptyState } from "../../newchat/chat-empty-state"; import { ChatMessageItem } from "../../newchat/chat-message-item"; -import { useGetTicketById , useUpdateTicket } from "@/shared/api/ticket"; -import { useGetConversation } from "@/shared/api/chat"; +import { useGetTicketById, useUpdateTicket } from "@/shared/api/ticket"; +import { useGetConversation } from "@/shared/api/chat-bot"; import Scrollbar from "@/shared/components/scrollbar"; import toast from "react-hot-toast"; import { IMessageItem } from "@/shared/types/chat-bot"; +import { m } from "framer-motion"; +import { CheckCircleIcon } from "lucide-react"; + interface TicketHistoryDataViewProps { ticket_id: string; @@ -29,16 +62,19 @@ export function TicketHistoryDataView({ ticket_id }: TicketHistoryDataViewProps) const { ticket, ticketLoading } = useGetTicketById(ticket_id); const { updateTicket } = useUpdateTicket(); const { conversation, conversationLoading } = useGetConversation(ticket?.conversation_id); + console.log('conversation', conversation); const scrollbarsRef = useRef(null); + const theme = useTheme(); + const [isScrolledToBottom, setIsScrolledToBottom] = useState(true); // Define status categories - const categories: { [key: string]: string } = { - OPEN: "Ouvert", - IN_PROGRESS: "En cours", - RESOLVED: "Résolu", - REJECTEED: "Rejeté", - WAITING: "En attente", - REASSIGNED: "Réassigné" + const categories: { [key: string]: { label: string, color: string, bgColor: string } } = { + OPEN: { label: "Ouvert", color: "#2196f3", bgColor: "rgba(33, 150, 243, 0.1)" }, + IN_PROGRESS: { label: "En cours", color: "#ff9800", bgColor: "rgba(255, 152, 0, 0.1)" }, + RESOLVED: { label: "Résolu", color: "#4caf50", bgColor: "rgba(76, 175, 80, 0.1)" }, + REJECTEED: { label: "Rejeté", color: "#f44336", bgColor: "rgba(244, 67, 54, 0.1)" }, + WAITING: { label: "En attente", color: "#9c27b0", bgColor: "rgba(156, 39, 176, 0.1)" }, + REASSIGNED: { label: "Réassigné", color: "#ff5722", bgColor: "rgba(255, 87, 34, 0.1)" } }; // For changing ticket status @@ -58,63 +94,115 @@ export function TicketHistoryDataView({ ticket_id }: TicketHistoryDataViewProps) ...ticket, ticketstatus: statusKey }); - toast.success(`Statut du ticket mis à jour: ${categories[statusKey]}`); + toast.success(`Statut du ticket mis à jour: ${categories[statusKey].label}`); } handleCloseMenu(); }; + const getStatusProgress = (status: string): number => { + const progressMap: { [key: string]: number } = { + OPEN: 20, + WAITING: 40, + IN_PROGRESS: 60, + REASSIGNED: 70, + RESOLVED: 100, + REJECTEED: 100 + }; + return progressMap[status] || 0; + }; + + const priorityLevels = ['Faible', 'Moyenne', 'Haute', 'Urgente']; + const priorityLevel = ticket?.priority || 1; + if (ticketLoading) { return ( - <Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", height: "100vh" }}> - <CircularProgress /> + <Box sx={{ + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + height: "100vh" + }}> + <CircularProgress size={60} thickness={4} /> + <Typography variant="h6" sx={{ mt: 2 }}> + Chargement des détails du ticket... + </Typography> </Box> ); } return ( - <Box sx={{ p: 3, minHeight: "100vh" }}> - <Card sx={{ mb: 3, boxShadow: 3, borderRadius: 2 }}> - <CardContent> - <Grid container spacing={2} alignItems="center"> - <Grid item> - <Avatar sx={{ bgcolor: "primary.main", width: 56, height: 56, fontSize: 24 }}> - {ticket?.title.charAt(0)} - </Avatar> - </Grid> - <Grid item> - <Typography variant="h5" gutterBottom> - Détails du Ticket - </Typography> - </Grid> - </Grid> - <Typography variant="body1" sx={{ mt: 2 }}> - {ticket?.title} - </Typography> - <Typography variant="body2" color="textSecondary"> - Créé Le : {new Date(ticket?.createdAt).toLocaleString()} - </Typography> - <Typography variant="body2" color="textSecondary"> - Mise À Jour Le : {new Date(ticket?.updatedAt).toLocaleString()} - </Typography> - - {/* Ticket status edit */} - <Box sx={{ mt: 2 }}> - <Typography variant="subtitle2" gutterBottom> - Statut du Ticket - </Typography> - <Chip - label={categories[ticket?.ticketstatus || "OPEN"]} - color={ - (ticket?.ticketstatus === "OPEN" && "info") || - (ticket?.ticketstatus === "IN_PROGRESS" && "warning") || - (ticket?.ticketstatus === "WAITING" && "success") || - (ticket?.ticketstatus === "REASSIGNED" && "warning") || - (ticket?.ticketstatus === "RESOLVED" && "primary") || - (ticket?.ticketstatus === "REJECTEED" && "error") || - "default" + <Box component={m.div} + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + transition={{ duration: 0.5 }} + sx={{ + p: 3, + minHeight: "100vh", + background: `linear-gradient(to bottom, ${theme.palette.background.default}, ${theme.palette.grey[50]})` + }} + > + <Grid container spacing={3}> + <Grid item xs={12} md={8}> + <Card + component={m.div} + initial={{ y: 20 }} + animate={{ y: 0 }} + transition={{ delay: 0.1 }} + sx={{ + mb: 3, + borderRadius: 2, + boxShadow: '0 8px 24px rgba(0,0,0,0.1)', + overflow: 'visible' + }} + > + <CardHeader + avatar={ + <Avatar + sx={{ + bgcolor: theme.palette.primary.main, + width: 64, + height: 64, + fontSize: 28, + boxShadow: '0 4px 12px rgba(0,0,0,0.15)' + }} + > + {ticket?.title?.charAt(0) || "T"} + </Avatar> + } + title={ + <Typography variant="h4" fontWeight="bold"> + {ticket?.title} + </Typography> } - onClick={handleOpenMenu} - sx={{ minWidth: 220, cursor: "pointer" }} + subheader={ + <Box sx={{ mt: 1, display: 'flex', alignItems: 'center' }}> + <Tooltip title="ID du ticket"> + <Chip + icon={<AssignmentIcon />} + label={`#${ticket_id.substring(0, 8)}`} + size="small" + sx={{ mr: 1, fontFamily: 'monospace' }} + /> + </Tooltip> + <Tooltip title="Statut actuel"> + <Chip + label={categories[ticket?.ticketstatus || "OPEN"].label} + onClick={handleOpenMenu} + sx={{ + cursor: "pointer", + bgcolor: categories[ticket?.ticketstatus || "OPEN"].bgColor, + color: categories[ticket?.ticketstatus || "OPEN"].color, + fontWeight: 'bold', + '&:hover': { + boxShadow: '0 2px 8px rgba(0,0,0,0.15)' + } + }} + /> + </Tooltip> + </Box> + } + /> <Menu anchorEl={anchorEl} @@ -122,51 +210,424 @@ export function TicketHistoryDataView({ ticket_id }: TicketHistoryDataViewProps) onClose={handleCloseMenu} anchorOrigin={{ vertical: "bottom", horizontal: "left" }} transformOrigin={{ vertical: "top", horizontal: "left" }} + slotProps={{ + paper: { + elevation: 3, + sx: { borderRadius: 2, minWidth: 200 } + } + }} > {Object.entries(categories).map(([key, value]) => ( - <MenuItem key={key} onClick={() => handleStatusChange(key)}> - {value} + <MenuItem + key={key} + onClick={() => handleStatusChange(key)} + sx={{ + py: 1.5, + '&:hover': { + bgcolor: value.bgColor, + color: value.color + } + }} + > + <Box sx={{ + width: 12, + height: 12, + borderRadius: '50%', + bgcolor: value.color, + mr: 1.5 + }} /> + {value.label} </MenuItem> ))} </Menu> - </Box> - </CardContent> - </Card> - <Divider sx={{ my: 3 }} /> - <Card sx={{ boxShadow: 3, borderRadius: 2 }}> - <CardContent> - <Typography variant="h5" gutterBottom> - Conversation - </Typography> - <Scrollbar - ref={scrollbarsRef} - sx={{ px: 3, pt: 2, pb: 2, flex: "1 1 auto", maxHeight: "400px" }} + <CardContent> + <Box sx={{ mb: 3 }}> + <LinearProgress + variant="determinate" + value={getStatusProgress(ticket?.ticketstatus || "OPEN")} + sx={{ + height: 8, + borderRadius: 4, + bgcolor: 'rgba(0,0,0,0.05)', + '& .MuiLinearProgress-bar': { + bgcolor: categories[ticket?.ticketstatus || "OPEN"].color + } + }} + /> + <Typography variant="caption" sx={{ mt: 0.5, display: 'block', textAlign: 'right' }}> + {getStatusProgress(ticket?.ticketstatus || "OPEN")}% complet + </Typography> + </Box> + + <Grid container spacing={3}> + <Grid item xs={12} md={6}> + <Paper + elevation={0} + sx={{ + p: 2, + bgcolor: 'rgba(0,0,0,0.02)', + borderRadius: 2, + height: '100%' + }} + > + <Typography variant="subtitle2" color="textSecondary"> + <AccessTimeIcon fontSize="small" sx={{ verticalAlign: 'middle', mr: 1 }} /> + Chronologie + </Typography> + <Box sx={{ mt: 1.5 }}> + <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> + <CreateIcon fontSize="small" color="primary" sx={{ mr: 1 }} /> + <Typography variant="body2"> + <strong>Créé le:</strong> {new Date(ticket?.createdAt).toLocaleString()} + </Typography> + </Box> + <Box sx={{ display: 'flex', alignItems: 'center' }}> + <UpdateIcon fontSize="small" color="primary" sx={{ mr: 1 }} /> + <Typography variant="body2"> + <strong>Mis à jour le:</strong> {new Date(ticket?.updatedAt).toLocaleString()} + </Typography> + </Box> + {ticket?.resolvedAt && ( + <Box sx={{ display: 'flex', alignItems: 'center', mt: 1 }}> + <CheckCircleIcon fontSize="small" color="success" /> + <Typography variant="body2"> + <strong>Résolu le:</strong> {new Date(ticket?.resolvedAt).toLocaleString()} + </Typography> + </Box> + )} + </Box> + </Paper> + </Grid> + <Grid item xs={12} md={6}> + <Paper + elevation={0} + sx={{ + p: 2, + bgcolor: 'rgba(0,0,0,0.02)', + borderRadius: 2, + height: '100%' + }} + > + <Typography variant="subtitle2" color="textSecondary"> + <CategoryIcon fontSize="small" sx={{ verticalAlign: 'middle', mr: 1 }} /> + Informations + </Typography> + <Box sx={{ mt: 1.5 }}> + <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> + <PersonIcon fontSize="small" color="primary" sx={{ mr: 1 }} /> + <Typography variant="body2"> + <strong>Demandeur:</strong> {ticket?.requester_email || "Non spécifié"} + </Typography> + </Box> + <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> + <PersonIcon fontSize="small" color="primary" sx={{ mr: 1 }} /> + <Typography variant="body2"> + <strong>Assigné à :</strong> {ticket?.assignee_email || "Non assigné"} + </Typography> + </Box> + <Box sx={{ display: 'flex', alignItems: 'center' }}> + <PriorityIcon fontSize="small" color={ + priorityLevel === 4 ? "error" : + priorityLevel === 3 ? "warning" : + priorityLevel === 2 ? "info" : "success" + } sx={{ mr: 1 }} /> + <Typography variant="body2"> + <strong>Priorité:</strong>{" "} + <Chip + label={priorityLevels[priorityLevel-1]} + size="small" + sx={{ + bgcolor: priorityLevel === 4 ? "error.light" : + priorityLevel === 3 ? "warning.light" : + priorityLevel === 2 ? "info.light" : "success.light", + color: priorityLevel === 4 ? "error.dark" : + priorityLevel === 3 ? "warning.dark" : + priorityLevel === 2 ? "info.dark" : "success.dark", + }} + /> + </Typography> + </Box> + </Box> + </Paper> + </Grid> + </Grid> + + {ticket?.description && ( + <Paper + elevation={0} + sx={{ + p: 2, + mt: 3, + bgcolor: 'rgba(0,0,0,0.02)', + borderRadius: 2, + borderLeft: `4px solid ${theme.palette.primary.main}` + }} + > + <Typography variant="subtitle2" color="textSecondary" gutterBottom> + Description du problème + </Typography> + <Typography variant="body1"> + {ticket.description} + </Typography> + </Paper> + )} + </CardContent> + </Card> + + <Card + component={m.div} + initial={{ y: 20, opacity: 0 }} + animate={{ y: 0, opacity: 1 }} + transition={{ delay: 0.2 }} + sx={{ + borderRadius: 2, + boxShadow: '0 8px 24px rgba(0,0,0,0.1)', + overflow: 'hidden' + }} > - {conversationLoading ? ( - <Box - sx={{ - display: "flex", - justifyContent: "center", - alignItems: "center", - height: "100%" + <CardHeader + avatar={ + <Badge + badgeContent={conversation?.messages?.length || 0} + color="primary" + max={99} + > + <Avatar sx={{ bgcolor: theme.palette.info.main }}> + <ChatIcon /> + </Avatar> + </Badge> + } + title={ + <Typography variant="h6"> + Conversation + </Typography> + } + subheader={ + conversation?.messages?.length + ? `${conversation.messages.length} message${conversation.messages.length > 1 ? 's' : ''}` + : "Aucun message" + } + /> + <Divider /> + <Box sx={{ height: 500, position: 'relative' }}> + {conversationLoading && ( + <LinearProgress sx={{ position: 'absolute', top: 0, left: 0, right: 0 }} /> + )} + <Scrollbar + ref={scrollbarsRef} + sx={{ + px: 3, + pt: 2, + pb: 2, + height: '100%', + bgcolor: theme.palette.background.default + }} + onScroll={(e: any) => { + const { scrollTop, scrollHeight, clientHeight } = e.target; + const isBottom = scrollHeight - scrollTop - clientHeight < 10; + setIsScrolledToBottom(isBottom); }} > - <CircularProgress /> - </Box> - ) : conversation?.messages.length ? ( - conversation.messages.map((message : IMessageItem, index : string) => ( - <ChatMessageItem key={index} message={message} /> - )) - ) : ( - <ChatEmptyState - title="No Conversation" - description="There are no messages in this conversation." - imgUrl="/assets/icons/empty/ic-content.svg" + {conversationLoading ? ( + <Box + sx={{ + display: "flex", + justifyContent: "center", + alignItems: "center", + height: "100%" + }} + > + <CircularProgress /> + </Box> + ) : conversation?.messages?.length ? ( + conversation.messages.map((message: IMessageItem, index: number) => ( + <ChatMessageItem + key={index} + message={message} + /> + )) + ) : ( + <ChatEmptyState + title="Aucune conversation" + description="Il n'y a pas encore de messages dans cette conversation." + imgUrl="/assets/icons/empty/ic-content.svg" + /> + )} + </Scrollbar> + </Box> + </Card> + </Grid> + + <Grid item xs={12} md={4}> + <Card + component={m.div} + initial={{ x: 20, opacity: 0 }} + animate={{ x: 0, opacity: 1 }} + transition={{ delay: 0.3 }} + sx={{ + borderRadius: 2, + boxShadow: '0 8px 24px rgba(0,0,0,0.1)', + position: 'sticky', + top: 24 + }} + > + <CardHeader + title="Historique du ticket" + avatar={ + <Avatar sx={{ bgcolor: theme.palette.success.main }}> + <AccessTimeIcon /> + </Avatar> + } + /> + <Divider /> + <CardContent sx={{ p: 0 }}> + <Timeline position="right" sx={{ p: 0, m: 0 }}> + <TimelineItem> + <TimelineOppositeContent color="text.secondary" sx={{ maxWidth: 120 }}> + {new Date(ticket?.createdAt).toLocaleDateString()} + </TimelineOppositeContent> + <TimelineSeparator> + <TimelineDot color="primary"> + <CreateIcon fontSize="small" /> + </TimelineDot> + <TimelineConnector /> + </TimelineSeparator> + <TimelineContent> + <Typography variant="body2" component="span"> + <strong>Ticket créé</strong> + </Typography> + <Typography variant="caption" display="block"> + {new Date(ticket?.createdAt).toLocaleTimeString()} + </Typography> + </TimelineContent> + </TimelineItem> + + {ticket?.status_history?.map((history: any, index: number) => ( + <TimelineItem key={index}> + <TimelineOppositeContent color="text.secondary" sx={{ maxWidth: 120 }}> + {new Date(history.date).toLocaleDateString()} + </TimelineOppositeContent> + <TimelineSeparator> + <TimelineDot + sx={{ + bgcolor: categories[history.status]?.color || 'grey' + }} + /> + <TimelineConnector /> + </TimelineSeparator> + <TimelineContent> + <Typography variant="body2" component="span"> + <strong>Statut changé en {categories[history.status]?.label}</strong> + </Typography> + <Typography variant="caption" display="block"> + {new Date(history.date).toLocaleTimeString()} + </Typography> + </TimelineContent> + </TimelineItem> + )) || ( + <TimelineItem> + <TimelineOppositeContent color="text.secondary" sx={{ maxWidth: 120 }}> + {new Date(ticket?.updatedAt).toLocaleDateString()} + </TimelineOppositeContent> + <TimelineSeparator> + <TimelineDot color="info"> + <UpdateIcon fontSize="small" /> + </TimelineDot> + <TimelineConnector /> + </TimelineSeparator> + <TimelineContent> + <Typography variant="body2" component="span"> + <strong>Statut actuel: {categories[ticket?.ticketstatus || "OPEN"].label}</strong> + </Typography> + <Typography variant="caption" display="block"> + {new Date(ticket?.updatedAt).toLocaleTimeString()} + </Typography> + </TimelineContent> + </TimelineItem> + )} + + {ticket?.resolvedAt && ( + <TimelineItem> + <TimelineOppositeContent color="text.secondary" sx={{ maxWidth: 120 }}> + {new Date(ticket.resolvedAt).toLocaleDateString()} + </TimelineOppositeContent> + <TimelineSeparator> + <TimelineDot color="success" /> + </TimelineSeparator> + <TimelineContent> + <Typography variant="body2" component="span"> + <strong>Ticket résolu</strong> + </Typography> + <Typography variant="caption" display="block"> + {new Date(ticket.resolvedAt).toLocaleTimeString()} + </Typography> + </TimelineContent> + </TimelineItem> + )} + </Timeline> + </CardContent> + </Card> + + {ticket?.related_tickets && ticket.related_tickets.length > 0 && ( + <Card + component={m.div} + initial={{ x: 20, opacity: 0 }} + animate={{ x: 0, opacity: 1 }} + transition={{ delay: 0.4 }} + sx={{ + mt: 3, + borderRadius: 2, + boxShadow: '0 8px 24px rgba(0,0,0,0.1)' + }} + > + <CardHeader + title="Tickets associés" + avatar={ + <Avatar sx={{ bgcolor: theme.palette.warning.main }}> + <EmailIcon /> + </Avatar> + } /> - )} - </Scrollbar> - </CardContent> - </Card> + <Divider /> + <CardContent> + {ticket.related_tickets.map((relatedTicket: any, index: number) => ( + <Box + key={index} + sx={{ + p: 1.5, + mb: 1, + borderRadius: 1, + bgcolor: 'background.paper', + boxShadow: '0 2px 8px rgba(0,0,0,0.05)', + '&:hover': { + bgcolor: 'action.hover' + } + }} + > + <Typography variant="subtitle2"> + {relatedTicket.title} + </Typography> + <Box sx={{ display: 'flex', alignItems: 'center', mt: 1 }}> + <Chip + size="small" + label={categories[relatedTicket.status]?.label || "Inconnu"} + sx={{ + bgcolor: categories[relatedTicket.status]?.bgColor || 'grey.100', + color: categories[relatedTicket.status]?.color || 'grey.700', + mr: 1 + }} + /> + <Typography variant="caption" color="text.secondary"> + #{relatedTicket.id.substring(0, 8)} + </Typography> + </Box> + </Box> + ))} + </CardContent> + </Card> + )} + </Grid> + </Grid> </Box> ); } \ No newline at end of file diff --git a/src/shared/sections/chat-bot/ticket/view/ticket-list-view.tsx b/src/shared/sections/chat-bot/ticket/view/ticket-list-view.tsx index 365678d4..79d377dd 100644 --- a/src/shared/sections/chat-bot/ticket/view/ticket-list-view.tsx +++ b/src/shared/sections/chat-bot/ticket/view/ticket-list-view.tsx @@ -29,7 +29,7 @@ import { import { useBoolean } from '@/hooks'; import Container from "@mui/material/Container"; -import { ISupportAgent } from '@/shared/types/user'; +import { ChatUser, ISupportAgent } from '@/shared/types/user'; import toast from 'react-hot-toast'; import Iconify from '@/components/iconify'; @@ -55,7 +55,7 @@ import { import { ITiketItem } from '@/shared/types/ticket'; import { useWebSocket } from '@/hooks/use-web-socket'; import { TicketStatistics } from '../ticket-status-statistic'; -import { useAuthContext } from '@/hooks'; +import { getCurrentChatUser } from '@/shared/api/chat-bot'; const HIDE_COLUMNS = { category: false }; const HIDE_COLUMNS_TOGGLABLE = ['category', 'actions']; @@ -65,11 +65,26 @@ const HIDE_COLUMNS_TOGGLABLE = ['category', 'actions']; export function TicketListView() { const confirmRows = useBoolean(); + const [userData, setUserData] = useState<ChatUser | null>(null); + + useEffect(() => { + async function fetchUser() { + const token = localStorage.getItem('chatbotToken'); + if (token) { + const data = await getCurrentChatUser(token); + setUserData(data); + } + } + fetchUser(); + }, []); + + const user = userData || {}; + const userid = user?.id; + const { tickets, ticketsLoading } = useGetTickets(); const { support, supportLoading } = useGetSupportUsers(); const { updateTicket } = useUpdateTicket(); - const { user } = useAuthContext(); - const userid = user?.id; + const router = useRouter(); const { messages, sendMessage } = useWebSocket(userid); @@ -111,7 +126,7 @@ export function TicketListView() { if (TicketId) { const ticketToUpdate = tableData.find(ticket => ticket.id === TicketId); if (ticketToUpdate && user) { - await updateTicket(TicketId, { + const response = await updateTicket(TicketId, { ...ticketToUpdate, ticketstatus: 'IN_PROGRESS', support_id: user.id }); @@ -152,9 +167,7 @@ export function TicketListView() { const CustomToolbarCallback = useCallback( () => ( <CustomToolbar - selectedRowIds={selectedRowIds} setFilterButtonEl={setFilterButtonEl} - onOpenConfirmDeleteRows={confirmRows.onTrue} data={tableData} /> ), @@ -165,37 +178,43 @@ export function TicketListView() { { field: 'title', headerName: 'Titre', - width: 300, + flex: 1, + minWidth: 200, renderCell: (params) => <RenderCellTicketTitle params={params} />, }, { field: 'ticketstatus', headerName: 'Statut', - width: 250, + flex: 0.8, + minWidth: 120, renderCell: (params) => <RenderCellStatus params={params} />, }, { field: 'createdAt', headerName: 'Date de création', - width: 200, + flex: 0.8, + minWidth: 150, renderCell: (params) => <RenderCellCreatedAt params={params} />, }, { field: 'support_id', headerName: 'Assigné à ', - width: 200, + flex: 0.8, + minWidth: 150, renderCell: (params) => <RenderCellTicketAsigned params={params} />, }, { field: 'assignedAt', headerName: "Date d'assignation", - width: 200, + flex: 0.8, + minWidth: 150, renderCell: (params) => <RenderCellAssignedAt params={params} />, }, { field: 'updatedAt', headerName: 'Dernière mise à jour', - width: 200, + flex: 0.8, + minWidth: 150, renderCell: (params) => <RenderCellUpdatedAt params={params} />, }, { @@ -236,11 +255,10 @@ export function TicketListView() { } ]; - const getTogglableColumns = () => - columns.filter((column) => !HIDE_COLUMNS_TOGGLABLE.includes(column.field)).map((column) => column.field); - return ( - <Container sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}> + <Container + maxWidth="xl" + sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}> <CustomBreadcrumbs heading="Voir Tous Les Tickets" links={[{ name: 'Tableau de bord' }, { name: 'Voir Tous Les Tickets' }]} @@ -251,7 +269,7 @@ export function TicketListView() { sx={{ flexGrow: { md: 2 }, display: { md: 'flex' }, - height: { xs: 800, md: 2 }, + height: { xs: 800, md: 2 }, width: { md: 1 }, flexDirection: { md: 'column' }, }} @@ -336,15 +354,11 @@ export function TicketListView() { // ---------------------------------------------------------------------- interface CustomToolbarProps { - selectedRowIds: GridRowSelectionModel; - onOpenConfirmDeleteRows: () => void; setFilterButtonEl: React.Dispatch<React.SetStateAction<HTMLButtonElement | null>>; data: ITiketItem[]; } function CustomToolbar({ - selectedRowIds, - onOpenConfirmDeleteRows, setFilterButtonEl, data, }: CustomToolbarProps) { @@ -353,18 +367,6 @@ function CustomToolbar({ <GridToolbarQuickFilter /> <GridToolbarFilterButton ref={setFilterButtonEl} /> <TicketStatistics data={data} /> - <Stack spacing={1} flexGrow={1} direction="row" alignItems="center" justifyContent="flex-end"> - {!!selectedRowIds.length ? ( - <Button - size="small" - color="error" - startIcon={<Iconify icon="solar:trash-bin-trash-bold" />} - onClick={onOpenConfirmDeleteRows} - > - Supprimer ({selectedRowIds.length}) - </Button> - ) : null} - </Stack> </GridToolbarContainer> ); } \ No newline at end of file diff --git a/src/shared/types/user.ts b/src/shared/types/user.ts index 018faf6c..d429d5c3 100644 --- a/src/shared/types/user.ts +++ b/src/shared/types/user.ts @@ -296,3 +296,5 @@ export type ISupportAgent = { } export type UserCountByStatus = [string, number]; + +export type ChatUser = Record<string, any> | null; \ No newline at end of file diff --git a/src/utils/token.ts b/src/utils/token.ts index a1d1cb26..d2c7ba15 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -33,3 +33,20 @@ export const getTokenInfo = (token: string): TokenPayload => { } }; + +export function isValidToken(accessToken: string) { + if (!accessToken) { + return false; + } + try { + const decoded = jwtDecode<TokenPayload>(accessToken); + if (!decoded) { + return false; + } + const currentTime = Date.now() / 1000; + return decoded.exp > currentTime; + } catch (error) { + console.error('Error during token validation:', error); + return false; + } +} \ No newline at end of file -- GitLab