From a21f301b3b4d4819dd3a29e605bb38e9025db167 Mon Sep 17 00:00:00 2001 From: VassiliMoskaljov Date: Tue, 17 Dec 2024 18:28:40 +0200 Subject: [PATCH 1/4] Moved common elements to analytics module --- DSL/Resql/get-chat-messages.sql | 39 ++++++++++ DSL/Ruuter/POST/agents/chats/messages/all.yml | 28 ++++++++ GUI/package.json | 1 + GUI/src/assets/logo-white.svg | 29 ++++++++ .../ButtonMessage/ButtonMessage.scss | 16 +++++ GUI/src/components/ButtonMessage/index.tsx | 19 +++++ GUI/src/components/ChatEvent/Markdownify.tsx | 41 +++++++++++ .../OptionMessage/OptionMessage.scss | 16 +++++ GUI/src/components/OptionMessage/index.tsx | 16 +++++ GUI/src/types/chat.ts | 19 +++++ GUI/src/types/message.ts | 72 +++++++++++++++++++ GUI/src/util/constants.ts | 19 +++++ GUI/src/util/parse-utils.ts | 23 ++++++ 13 files changed, 338 insertions(+) create mode 100644 DSL/Resql/get-chat-messages.sql create mode 100644 DSL/Ruuter/POST/agents/chats/messages/all.yml create mode 100644 GUI/src/assets/logo-white.svg create mode 100644 GUI/src/components/ButtonMessage/ButtonMessage.scss create mode 100644 GUI/src/components/ButtonMessage/index.tsx create mode 100644 GUI/src/components/ChatEvent/Markdownify.tsx create mode 100644 GUI/src/components/OptionMessage/OptionMessage.scss create mode 100644 GUI/src/components/OptionMessage/index.tsx create mode 100644 GUI/src/types/message.ts create mode 100644 GUI/src/util/constants.ts create mode 100644 GUI/src/util/parse-utils.ts diff --git a/DSL/Resql/get-chat-messages.sql b/DSL/Resql/get-chat-messages.sql new file mode 100644 index 00000000..9b00c2d7 --- /dev/null +++ b/DSL/Resql/get-chat-messages.sql @@ -0,0 +1,39 @@ +WITH MaxMessages AS ( + SELECT max(id) AS maxId + FROM message + WHERE chat_base_id = :chatId + GROUP BY base_id +), +LatestActiveUser AS ( + SELECT + u.id_code, u.created, u.csa_title + FROM + "user" u INNER JOIN ( + SELECT iu.id_code, max(created) AS MaxCreated + FROM "user" iu + WHERE iu.status = 'active' + GROUP BY iu.id_code + ) iju ON iju.id_code = u.id_code AND iju.MaxCreated = u.created +) +SELECT m.base_id AS id, + m.chat_base_id AS chat_id, + m.content, + m.buttons, + m.options, + m.event, + m.author_id, + m.author_timestamp, + m.author_first_name, + m.author_last_name, + m.author_role, + m.forwarded_by_user, + m.forwarded_from_csa, + m.forwarded_to_csa, + rating, + m.created, + updated, + u.csa_title +FROM message m +LEFT JOIN LatestActiveUser u ON m.author_id = u.id_code +JOIN MaxMessages ON m.id = maxId +ORDER BY created ASC; diff --git a/DSL/Ruuter/POST/agents/chats/messages/all.yml b/DSL/Ruuter/POST/agents/chats/messages/all.yml new file mode 100644 index 00000000..b244801f --- /dev/null +++ b/DSL/Ruuter/POST/agents/chats/messages/all.yml @@ -0,0 +1,28 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'ALL'" + method: post + accepts: json + returns: json + namespace: backoffice + allowlist: + body: + - field: chatId + type: string + description: "Body field 'chatId'" + +extractRequestData: + assign: + chatId: ${incoming.body.chatId} + +getUnavailableEndedChats: + call: http.post + args: + url: "[#CHATBOT_RESQL]/get-chat-messages" + body: + chatId: ${chatId} + result: res + +return_result: + return: ${res.response.body} diff --git a/GUI/package.json b/GUI/package.json index 74e7ac3f..cc0fe33d 100644 --- a/GUI/package.json +++ b/GUI/package.json @@ -24,6 +24,7 @@ "downshift": "^7.2.0", "eslint-plugin-prettier": "^4.2.1", "file-saver": "^2.0.5", + "markdown-to-jsx": "^7.5.0", "howler": "^2.2.4", "i18next": "^22.4.9", "i18next-browser-languagedetector": "^7.0.1", diff --git a/GUI/src/assets/logo-white.svg b/GUI/src/assets/logo-white.svg new file mode 100644 index 00000000..20257361 --- /dev/null +++ b/GUI/src/assets/logo-white.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + diff --git a/GUI/src/components/ButtonMessage/ButtonMessage.scss b/GUI/src/components/ButtonMessage/ButtonMessage.scss new file mode 100644 index 00000000..b4ca117d --- /dev/null +++ b/GUI/src/components/ButtonMessage/ButtonMessage.scss @@ -0,0 +1,16 @@ +@import 'src/styles/tools/color'; + +.button-container { + display: flex; + gap: 12px; + margin: 0.3rem 0 0.7rem 0; + + span { + padding: 0 0.5rem; + background-color: get-color(sea-green-12); + color: get-color(white); + border-radius: 8px; + box-shadow: 2px 1px 4px rgb(159, 159, 159); + opacity: 0.7; + } +} diff --git a/GUI/src/components/ButtonMessage/index.tsx b/GUI/src/components/ButtonMessage/index.tsx new file mode 100644 index 00000000..9f7fbe75 --- /dev/null +++ b/GUI/src/components/ButtonMessage/index.tsx @@ -0,0 +1,19 @@ +import { FC } from 'react'; +import { MessageButton } from 'types/message'; +import './ButtonMessage.scss'; + +type ButtonMessageProps = { + buttons: MessageButton[]; +}; + +const ButtonMessage: FC = ({ buttons }) => { + return ( +
+ {buttons.map(({title, payload}) => + {title}({payload}) + )} +
+ ); +}; + +export default ButtonMessage; diff --git a/GUI/src/components/ChatEvent/Markdownify.tsx b/GUI/src/components/ChatEvent/Markdownify.tsx new file mode 100644 index 00000000..d049d2e2 --- /dev/null +++ b/GUI/src/components/ChatEvent/Markdownify.tsx @@ -0,0 +1,41 @@ +import React, { useState } from 'react'; +import Markdown from 'markdown-to-jsx'; + +interface MarkdownifyProps { + message: string | undefined; +} + +const LinkPreview: React.FC<{ href: string }> = ({ href }) => { + const [hasError, setHasError] = useState(false); + return !hasError ? ( + Image Preview setHasError(true)} + /> + ) : ( + + {href} + + ); +}; + +const Markdownify: React.FC = ({ message }) => ( +
+ + {message ?? ''} + +
+); + +export default Markdownify; diff --git a/GUI/src/components/OptionMessage/OptionMessage.scss b/GUI/src/components/OptionMessage/OptionMessage.scss new file mode 100644 index 00000000..4f30663a --- /dev/null +++ b/GUI/src/components/OptionMessage/OptionMessage.scss @@ -0,0 +1,16 @@ +@import 'src/styles/tools/color'; + +.option-container { + display: flex; + gap: 12px; + margin: 0.3rem 0 0.7rem 0; + + span { + padding: 0 0.5rem; + background-color: get-color(sea-green-12); + color: get-color(white); + border-radius: 8px; + box-shadow: 2px 1px 4px rgb(159, 159, 159); + opacity: 0.7; + } +} diff --git a/GUI/src/components/OptionMessage/index.tsx b/GUI/src/components/OptionMessage/index.tsx new file mode 100644 index 00000000..0f6a0bc6 --- /dev/null +++ b/GUI/src/components/OptionMessage/index.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react'; +import './OptionMessage.scss'; + +type OptionMessageProps = { + options: string[]; +}; + +const OptionMessage: FC = ({ options }) => { + return ( +
+ {options.map(option => {option})} +
+ ); +}; + +export default OptionMessage; diff --git a/GUI/src/types/chat.ts b/GUI/src/types/chat.ts index 9b9c2106..1a2861df 100644 --- a/GUI/src/types/chat.ts +++ b/GUI/src/types/chat.ts @@ -4,6 +4,10 @@ export enum CHAT_STATUS { REDIRECTED = 'REDIRECTED', } +export enum BACKOFFICE_NAME { + DEFAULT = 'Bürokratt' +} + export enum CHAT_EVENTS { ANSWERED = 'answered', TERMINATED = 'terminated', @@ -33,6 +37,21 @@ export enum CHAT_EVENTS { REQUESTED_CHAT_FORWARD_ACCEPTED = 'requested-chat-forward-accepted', REQUESTED_CHAT_FORWARD_REJECTED = 'requested-chat-forward-rejected', READ = 'message-read', + ASK_TO_FORWARD_TO_CSA = 'ask_to_forward_to_csa', + FORWARDED_TO_BACKOFFICE = 'forwarded_to_backoffice', + CONTINUE_CHATTING_WITH_BOT = 'continue_chatting_with_bot', + UNAVAILABLE_CONTACT_INFORMATION_FULFILLED = 'unavailable-contact-information-fulfilled', + CONTACT_INFORMATION_SKIPPED = 'contact-information-skipped', + UNAVAILABLE_ORGANIZATION = 'unavailable_organization', + UNAVAILABLE_CSAS = 'unavailable_csas', + UNAVAILABLE_CSAS_ASK_CONTACTS = 'unavailable_csas_ask_contacts', + UNAVAILABLE_HOLIDAY = 'unavailable_holiday', + ASSIGN_PENDING_CHAT_CSA = 'pending-assigned', + PENDING_USER_REACHED = 'user-reached', + PENDING_USER_NOT_REACHED = 'user-not-reached', + USER_AUTHENTICATED = 'user-authenticated', + WAITING_VALIDATION = 'waiting_validation', + APPROVED_VALIDATION = 'approved_validation', } export interface Chat { diff --git a/GUI/src/types/message.ts b/GUI/src/types/message.ts new file mode 100644 index 00000000..811f8124 --- /dev/null +++ b/GUI/src/types/message.ts @@ -0,0 +1,72 @@ +export interface UseSendAttachment { + successCb?: (data: any) => void; + errorCb?: (error: any) => void; + data: { + chatId: string, + name: string, + type: string, + size: string, + base64: string, + } +} + +export interface Attachment { + chatId: string; + name: string; + type: AttachmentTypes; + size: number; + base64: string; +} + +export interface Message { + id?: string; + chatId: string; + content?: string; + event?: string; + csaTitle?: string; + authorId?: string; + authorTimestamp: string; + authorFirstName: string; + authorLastName?: string; + authorRole: string; + forwardedByUser: string; + forwardedFromCsa: string; + forwardedToCsa: string; + rating?: string; + created?: string; + preview?: string; + updated?: string; + buttons?: string; + options?: string; +} + +export interface MessagePreviewSseResponse { + data: Message; + origin: string; + type: string; +} + +export enum AttachmentTypes { + PDF = 'application/pdf', + PNG = 'image/png', + JPEG = 'image/jpeg', + TXT = 'text/plain', + DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ODT = 'application/vnd.oasis.opendocument.text', + XLSX = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ODS = 'ods', + CDOC = 'application/x-cdoc', + ASICE = 'application/vnd.etsi.asic-e+zip', + MP3 = 'audio/mpeg', + WAV = 'audio/wav', + M4A = 'audio/x-m4a', + MP4 = 'video/mp4', + WEBM = 'video/webm', + OGG = 'video/ogg', + MOV = 'video/quicktime', +} + +export interface MessageButton { + title: string; + payload: string; +} diff --git a/GUI/src/util/constants.ts b/GUI/src/util/constants.ts new file mode 100644 index 00000000..bc9be5bc --- /dev/null +++ b/GUI/src/util/constants.ts @@ -0,0 +1,19 @@ +export const MESSAGE_FILE_SIZE_LIMIT = 10_000_000; + +export enum ROLES { + ROLE_ADMINISTRATOR = 'ROLE_ADMINISTRATOR', + ROLE_SERVICE_MANAGER = 'ROLE_SERVICE_MANAGER', + ROLE_CUSTOMER_SUPPORT_AGENT = 'ROLE_CUSTOMER_SUPPORT_AGENT', + ROLE_CHATBOT_TRAINER = 'ROLE_CHATBOT_TRAINER', + ROLE_ANALYST = 'ROLE_ANALYST', + ROLE_UNAUTHENTICATED = 'ROLE_UNAUTHENTICATED', +} + +export enum RUUTER_ENDPOINTS { + SEND_ATTACHMENT= '/attachments/add' +} + +export enum AUTHOR_ROLES { + END_USER = 'end-user', + BACKOFFICE_USER = 'backoffice-user', +} diff --git a/GUI/src/util/parse-utils.ts b/GUI/src/util/parse-utils.ts new file mode 100644 index 00000000..988aa09c --- /dev/null +++ b/GUI/src/util/parse-utils.ts @@ -0,0 +1,23 @@ +import { Message, MessageButton } from "types/message"; + +export const parseOptions = (message: Message): string[] => { + try { + if(!message?.options || message.options === '') + return []; + return JSON.parse(message.options) as string[]; + } catch(e) { + console.error(e); + return []; + } +} + +export const parseButtons = (message: Message): MessageButton[] => { + try { + if(!message?.buttons || message.buttons === '') + return []; + return JSON.parse(message.buttons) as MessageButton[]; + } catch(e) { + console.error(e); + return []; + } +} From 19a8ff0da37629c6a03b7ce62e48bc863e6417bd Mon Sep 17 00:00:00 2001 From: VassiliMoskaljov Date: Wed, 18 Dec 2024 11:44:32 +0200 Subject: [PATCH 2/4] Updated base elements and stylings, added and updated missing queries --- DSL/Resql/get-chat-by-id.sql | 57 +++++ DSL/Ruuter/POST/agents/chats/messages/all.yml | 2 +- DSL/Ruuter/POST/chats/get.yml | 29 +++ GUI/src/components/Card/Card.scss | 43 ++++ GUI/src/components/Card/index.tsx | 45 ++-- GUI/src/components/ChatEvent/Chat.scss | 40 ++++ GUI/src/components/ChatEvent/index.tsx | 211 ++++++++++++++++ GUI/src/components/ChatsTable/ChatsTable.scss | 26 ++ GUI/src/components/ChatsTable/index.tsx | 198 +++++++++------ .../components/HistoricalChat/ChatMessage.tsx | 66 +++++ .../HistoricalChat/HistoricalChat.scss | 225 ++++++++++++++++++ GUI/src/components/HistoricalChat/index.tsx | 203 ++++++++++++++++ GUI/src/i18n/en/common.json | 175 +++++++++++++- GUI/src/i18n/et/common.json | 173 +++++++++++++- GUI/tsconfig.json | 25 +- 15 files changed, 1413 insertions(+), 105 deletions(-) create mode 100644 DSL/Resql/get-chat-by-id.sql create mode 100644 DSL/Ruuter/POST/chats/get.yml create mode 100644 GUI/src/components/ChatEvent/Chat.scss create mode 100644 GUI/src/components/ChatEvent/index.tsx create mode 100644 GUI/src/components/ChatsTable/ChatsTable.scss create mode 100644 GUI/src/components/HistoricalChat/ChatMessage.tsx create mode 100644 GUI/src/components/HistoricalChat/HistoricalChat.scss create mode 100644 GUI/src/components/HistoricalChat/index.tsx diff --git a/DSL/Resql/get-chat-by-id.sql b/DSL/Resql/get-chat-by-id.sql new file mode 100644 index 00000000..2426be7a --- /dev/null +++ b/DSL/Resql/get-chat-by-id.sql @@ -0,0 +1,57 @@ +SELECT c.base_id AS id, + c.customer_support_id, + c.customer_support_display_name, + c.end_user_id, + c.end_user_first_name, + c.end_user_last_name, + c.status, + c.feedback_text, + c.feedback_rating, + c.end_user_email, + c.end_user_phone, + c.end_user_os, + c.end_user_url, + c.created, + c.updated, + c.ended, + c.external_id, + c.received_from, + c.received_from_name, + c.forwarded_to_name, + c.forwarded_to, + (CASE WHEN (SELECT value FROM configuration WHERE key = 'is_csa_title_visible' AND configuration.id IN (SELECT max(id) from configuration GROUP BY key) AND deleted = false) = 'true' + THEN c.csa_title ELSE '' END) AS csa_title, + m.content AS last_message, + m.updated AS last_message_timestamp +FROM ( + SELECT + base_id, + customer_support_id, + customer_support_display_name, + end_user_id, + end_user_first_name, + end_user_last_name, + status, + feedback_text, + feedback_rating, + end_user_email, + end_user_phone, + end_user_os, + end_user_url, + created, + updated, + ended, + external_id, + received_from, + received_from_name, + forwarded_to_name, + forwarded_to, + csa_title + FROM chat + WHERE base_id = :id + ORDER BY updated DESC + LIMIT 1 +) AS c +JOIN message AS m ON c.base_id = m.chat_base_id +ORDER BY m.updated DESC +LIMIT 1; diff --git a/DSL/Ruuter/POST/agents/chats/messages/all.yml b/DSL/Ruuter/POST/agents/chats/messages/all.yml index b244801f..0078ca1c 100644 --- a/DSL/Ruuter/POST/agents/chats/messages/all.yml +++ b/DSL/Ruuter/POST/agents/chats/messages/all.yml @@ -19,7 +19,7 @@ extractRequestData: getUnavailableEndedChats: call: http.post args: - url: "[#CHATBOT_RESQL]/get-chat-messages" + url: "[#ANALYTICS_RESQL]/get-chat-messages" body: chatId: ${chatId} result: res diff --git a/DSL/Ruuter/POST/chats/get.yml b/DSL/Ruuter/POST/chats/get.yml new file mode 100644 index 00000000..104a2d4f --- /dev/null +++ b/DSL/Ruuter/POST/chats/get.yml @@ -0,0 +1,29 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'GET'" + method: post + accepts: json + returns: json + namespace: backoffice + allowlist: + body: + - field: chatId + type: string + description: "Body field 'chatId'" + +extractRequestData: + assign: + chatId: ${incoming.body.chatId} + +getChatById: + call: http.post + args: + url: "[#ANALYTICS_RESQL]/get-chat-by-id" + body: + id: ${chatId} + result: res + +return_result: + return: ${res.response.body[0]} + next: end diff --git a/GUI/src/components/Card/Card.scss b/GUI/src/components/Card/Card.scss index cf4ae6f6..4a67007f 100644 --- a/GUI/src/components/Card/Card.scss +++ b/GUI/src/components/Card/Card.scss @@ -4,9 +4,25 @@ @import 'src/styles/settings/variables/typography'; .card { + $self: &; background-color: get-color(white); border: 1px solid get-color(black-coral-2); border-radius: $veera-radius-s; + max-height: 80vh; + overflow-y: visible; + + &--borderless { + border: 0; + border-radius: 0; + + #{$self}__header { + border-radius: 0; + } + } + + &--scrollable { + overflow-y: auto; + } &__header, &__body, @@ -18,9 +34,36 @@ border-bottom: 1px solid get-color(black-coral-2); background-color: get-color(extra-light); border-radius: $veera-radius-s $veera-radius-s 0 0; + + &.white { + background-color: white; + } + } + + &__body { + &.divided { + display: flex; + flex-direction: column; + padding-left: 0px; + padding-right: 0px; + + > :not(:last-child) { + margin-bottom: get-spacing(haapsalu); + border-bottom: 1px solid get-color(black-coral-2); + padding-bottom: get-spacing(haapsalu); + padding-left: get-spacing(haapsalu); + } + + > :is(:last-child) { + padding-left: get-spacing(haapsalu); + } + } } &__footer { border-top: 1px solid get-color(black-coral-2); + position: sticky; + bottom: 0; + background-color: get-color(white); } } diff --git a/GUI/src/components/Card/index.tsx b/GUI/src/components/Card/index.tsx index 779c37c8..0747dd1b 100644 --- a/GUI/src/components/Card/index.tsx +++ b/GUI/src/components/Card/index.tsx @@ -1,24 +1,39 @@ import React, { FC, PropsWithChildren, ReactNode } from 'react'; import './Card.scss'; +import clsx from "clsx"; type CardProps = { - header?: ReactNode; - footer?: ReactNode; -} + header?: ReactNode; + footer?: ReactNode; + borderless?: boolean; + isHeaderLight?: boolean; + isBodyDivided?: boolean; + isScrollable?: boolean; +}; -const Card: FC> = ({ header, footer, children }) => { - return ( -
- {header &&
{header}
} -
- {children} -
- {footer && ( -
{footer}
- )} -
- ); +const Card: FC> = ({ + header, + footer, + borderless, + isHeaderLight, + isBodyDivided, + isScrollable = false, + children, + }) => { + return ( +
+ {header && ( +
+ {header} +
+ )} +
+ {children} +
+ {footer &&
{footer}
} +
+ ); }; export default Card; diff --git a/GUI/src/components/ChatEvent/Chat.scss b/GUI/src/components/ChatEvent/Chat.scss new file mode 100644 index 00000000..c55d5712 --- /dev/null +++ b/GUI/src/components/ChatEvent/Chat.scss @@ -0,0 +1,40 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.active-chat { + $self: &; + display: flex; + justify-content: flex-end; + position: relative; + height: 100%; + + &__event-message { + display: flex; + position: relative; + isolation: isolate; + padding: 6px 0; + + p { + color: get-color(black-coral-12); + font-size: $veera-font-size-80; + line-height: $veera-line-height-500; + padding: 4px 8px 4px 0; + background-color: get-color(white); + } + + &::after { + content: ''; + display: block; + height: 1px; + background-color: get-color(black-coral-2); + position: absolute; + top: 50%; + left: 56px; + right: 0; + transform: translateY(-50%); + z-index: -1; + } + } +} diff --git a/GUI/src/components/ChatEvent/index.tsx b/GUI/src/components/ChatEvent/index.tsx new file mode 100644 index 00000000..89d1390b --- /dev/null +++ b/GUI/src/components/ChatEvent/index.tsx @@ -0,0 +1,211 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Message } from '../../types/message'; +import { CHAT_EVENTS } from '../../types/chat'; +import { format } from 'date-fns'; +import './Chat.scss'; + +type ChatEventProps = { + message: Message; +}; + +const ChatEvent: FC = ({ message }) => { + const { t } = useTranslation(); + const { + event, + authorTimestamp, + forwardedByUser, + forwardedFromCsa, + forwardedToCsa, + } = message; + + let EVENT_PARAMS; + + switch (event) { + case CHAT_EVENTS.REDIRECTED: + if (forwardedByUser === forwardedFromCsa) { + EVENT_PARAMS = t('chat.redirectedMessageByOwner', { + from: forwardedFromCsa, + to: forwardedToCsa, + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + } else if (forwardedByUser === forwardedToCsa) { + EVENT_PARAMS = t('chat.redirectedMessageClaimed', { + from: forwardedFromCsa, + to: forwardedToCsa, + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + } else { + EVENT_PARAMS = t('chat.redirectedMessage', { + user: forwardedByUser, + from: forwardedFromCsa, + to: forwardedToCsa, + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + } + + break; + case CHAT_EVENTS.ANSWERED: + EVENT_PARAMS = t('chat.events.answered', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.TERMINATED: + EVENT_PARAMS = t('chat.events.terminated', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.CLIENT_LEFT: + EVENT_PARAMS = t('chat.events.client-left', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.CLIENT_LEFT_WITH_ACCEPTED: + EVENT_PARAMS = t('chat.events.client-left-with-accepted', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.CLIENT_LEFT_WITH_NO_RESOLUTION: + EVENT_PARAMS = t('chat.events.client-left-with-no-resolution', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.CLIENT_LEFT_FOR_UNKNOWN_REASONS: + EVENT_PARAMS = t('chat.events.client-left-for-unknown-reason', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.ACCEPTED: + EVENT_PARAMS = t('chat.events.accepted', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.HATE_SPEECH: + EVENT_PARAMS = t('chat.events.hate-speech', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.OTHER: + EVENT_PARAMS = t('chat.events.other', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.RESPONSE_SENT_TO_CLIENT_EMAIL: + EVENT_PARAMS = t('chat.events.response-sent-to-client-email', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.GREETING: + EVENT_PARAMS = t('chat.events.greeting', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.REQUESTED_AUTHENTICATION: + EVENT_PARAMS = t('chat.events.requested-authentication', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.AUTHENTICATION_SUCCESSFUL: + EVENT_PARAMS = t('chat.events.authentication-successful', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.AUTHENTICATION_FAILED: + EVENT_PARAMS = t('chat.events.authentication-failed', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.ASK_PERMISSION: + EVENT_PARAMS = t('chat.events.ask-permission', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.ASK_PERMISSION_ACCEPTED: + EVENT_PARAMS = t('chat.events.ask-permission-accepted', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.ASK_PERMISSION_REJECTED: + EVENT_PARAMS = t('chat.events.ask-permission-rejected', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.ASK_PERMISSION_IGNORED: + EVENT_PARAMS = t('chat.events.ask-permission-ignored', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.RATING: + EVENT_PARAMS = t('chat.events.rating', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.CONTACT_INFORMATION: + EVENT_PARAMS = t('chat.events.contact-information', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.CONTACT_INFORMATION_REJECTED: + EVENT_PARAMS = t('chat.events.contact-information-rejected', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.CONTACT_INFORMATION_FULFILLED: + EVENT_PARAMS = t('chat.events.contact-information-fulfilled', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.REQUESTED_CHAT_FORWARD: + EVENT_PARAMS = t('chat.events.requested-chat-forward', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.REQUESTED_CHAT_FORWARD_ACCEPTED: + EVENT_PARAMS = t('chat.events.requested-chat-forward-accepted', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.REQUESTED_CHAT_FORWARD_REJECTED: + EVENT_PARAMS = t('chat.events.requested-chat-forward-rejected', { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + case CHAT_EVENTS.ASSIGN_PENDING_CHAT_CSA: + EVENT_PARAMS = t('chat.events.pending-assigned', { + name: message.authorFirstName ?? 'CSA', + }); + break; + case CHAT_EVENTS.PENDING_USER_REACHED: + EVENT_PARAMS = t('chat.events.user-reached', { + name: message.authorFirstName ?? 'CSA', + }); + break; + case CHAT_EVENTS.PENDING_USER_NOT_REACHED: + EVENT_PARAMS = t('chat.events.user-not-reached', { + name: message.authorFirstName ?? 'CSA', + }); + break; + case CHAT_EVENTS.USER_AUTHENTICATED: + EVENT_PARAMS = t('chat.events.user-authenticated', { + name: `${message.authorFirstName ?? ''} ${ + message.authorLastName ?? '' + }`, + date: format(new Date(message.authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + default: + EVENT_PARAMS = t(`chat.events.${event?.toLowerCase()}`, { + date: format(new Date(authorTimestamp), 'dd.MM.yyyy HH:mm:ss'), + }); + break; + } + + return ( +
+

{EVENT_PARAMS}

+
+ ); +}; + +export default ChatEvent; diff --git a/GUI/src/components/ChatsTable/ChatsTable.scss b/GUI/src/components/ChatsTable/ChatsTable.scss new file mode 100644 index 00000000..42b5f2cb --- /dev/null +++ b/GUI/src/components/ChatsTable/ChatsTable.scss @@ -0,0 +1,26 @@ +.card-drawer-container { + display: flex; + width: 100%; + height: 100vh; + box-sizing: border-box; +} + +.card-wrapper { + flex: 1; + overflow: auto; + transition: flex 0.3s ease; +} + +.drawer-container { + width: 700px; + height: 100%; + flex-shrink: 0; + overflow-y: auto; + box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1); +} + +.drawer-container > * { + position: static !important; + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/GUI/src/components/ChatsTable/index.tsx b/GUI/src/components/ChatsTable/index.tsx index 31504f9a..a747f6a3 100644 --- a/GUI/src/components/ChatsTable/index.tsx +++ b/GUI/src/components/ChatsTable/index.tsx @@ -1,96 +1,142 @@ -import { createColumnHelper, PaginationState, CellContext, SortingState } from '@tanstack/react-table'; -import React, { useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { MdOutlineRemoveRedEye } from 'react-icons/md'; -import { getLinkToChat } from '../../resources/api-constants'; -import { Chat } from '../../types/chat'; -import { formatDate } from '../../util/charts-utils'; +import {CellContext, createColumnHelper, PaginationState, SortingState} from '@tanstack/react-table'; +import React, {useEffect, useMemo, useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import {MdOutlineRemoveRedEye} from 'react-icons/md'; +import {Chat} from '../../types/chat'; +import {formatDate} from '../../util/charts-utils'; import Button from '../Button'; import Card from '../Card'; import DataTable from '../DataTable'; import Icon from '../Icon'; import Track from '../Track'; +import Drawer from "../Drawer"; +import HistoricalChat from "../HistoricalChat"; +import './ChatsTable.scss'; +import {useMutation} from "@tanstack/react-query"; +import {analyticsApi} from "../services/api"; type Props = { - dataSource: Chat[]; - startDate?: string; - endDate?: string; - pagination?: PaginationState; - sorting?: SortingState; - setSorting?: (state: SortingState) => void; - setPagination?: (state: PaginationState) => void; + dataSource: Chat[]; + startDate?: string; + endDate?: string; + pagination?: PaginationState; + sorting?: SortingState; + setSorting?: (state: SortingState) => void; + setPagination?: (state: PaginationState) => void; }; const ChatsTable = (props: Props) => { - const [chats, setChats] = useState([]); + const [chats, setChats] = useState([]); + const [selectedChat, setSelectedChat] = useState(null); + const columnHelper = createColumnHelper(); + const {t} = useTranslation(); - const columnHelper = createColumnHelper(); - const { t } = useTranslation(); + useEffect(() => { + const fetchChats = async () => { + setChats(props.dataSource); + }; + fetchChats().catch(console.error); + }, [props.dataSource]); - useEffect(() => { - const fetchChats = async () => { - setChats(props.dataSource); - }; - fetchChats().catch(console.error); - }, [props.dataSource]); + const dateTimeFormat = (props: CellContext) => + formatDate(new Date(props.getValue()), 'd. MMM yyyy HH:mm:ss'); - const dateTimeFormat = (props: CellContext) => - formatDate(new Date(props.getValue()), 'd. MMM yyyy HH:mm:ss'); - const feedbackViewButton = (dataTableProps: any) => ( - - - - ); + const fillChatData = (data: any) => { + getChatById.mutate(data.row.original.baseId ?? ''); + setSelectedChat(selectedChat) + } - const chatColumns = useMemo( - () => [ - columnHelper.accessor('baseId', { - header: 'ID', - }), - columnHelper.accessor('feedback', { - header: t('feedback.comment') ?? '', - }), - columnHelper.accessor('created', { - header: t('feedback.startTime') ?? '', - cell: dateTimeFormat, - }), - columnHelper.accessor('ended', { - header: t('feedback.endTime') ?? '', - cell: dateTimeFormat, - }), - columnHelper.accessor('rating', { - header: t('chart.rating') ?? '', - }), - columnHelper.display({ - id: 'detail', - cell: feedbackViewButton, - meta: { - size: '1%', + const feedbackViewButton = (dataTableProps: any) => ( + + ); + + const getChatById = useMutation({ + mutationFn: (chatId: string) => + analyticsApi.post('chats/get', { + chatId: chatId, + }), + onSuccess: (res: any) => { + setSelectedChat(res.data.response); }, - }), - ], - [] - ); + }); + + const chatColumns = useMemo( + () => [ + columnHelper.accessor('baseId', { + header: 'ID', + }), + columnHelper.accessor('feedback', { + header: t('feedback.comment') ?? '', + }), + columnHelper.accessor('created', { + header: t('feedback.startTime') ?? '', + cell: dateTimeFormat, + }), + columnHelper.accessor('ended', { + header: t('feedback.endTime') ?? '', + cell: dateTimeFormat, + }), + columnHelper.accessor('rating', { + header: t('chart.rating') ?? '', + }), + columnHelper.display({ + id: 'detail', + cell: feedbackViewButton, + meta: { + size: '3%', + sticky: 'right' + }, + }), + ], + [] + ); + + return ( - return ( - - - - ); +
+
+ + + +
+ {selectedChat && ( +
+ setSelectedChat(null)} + > + + +
+ )} +
+ ); }; export default ChatsTable; diff --git a/GUI/src/components/HistoricalChat/ChatMessage.tsx b/GUI/src/components/HistoricalChat/ChatMessage.tsx new file mode 100644 index 00000000..adcc592b --- /dev/null +++ b/GUI/src/components/HistoricalChat/ChatMessage.tsx @@ -0,0 +1,66 @@ +import { FC, useMemo } from 'react'; +import { format } from 'date-fns'; +import { Message } from 'types/message'; +import Markdownify from '../ChatEvent/Markdownify'; +import { parseButtons, parseOptions } from '../../util/parse-utils'; +import ButtonMessage from '..//ButtonMessage'; +import OptionMessage from '..//OptionMessage'; +import {useToast} from "../../hooks/useToast"; +import {useTranslation} from "react-i18next"; + +type ChatMessageProps = { + message: Message; + onMessageClick?: (message: Message) => void; +}; + +const ChatMessage: FC = ({ message, onMessageClick }) => { + const buttons = useMemo(() => parseButtons(message), [message.buttons]); + const options = useMemo(() => parseOptions(message), [message.options]); + const { t } = useTranslation(); + const toast = useToast(); + + const handleContextMenu = (event: React.MouseEvent) => { + event.preventDefault(); + const content = message.content ?? ''; + navigator.clipboard + .writeText(content) + .then(() => { + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.copied'), + }); + }) + .catch((err) => { + toast.open({ + type: 'error', + title: t('global.notification'), + message: err?.message, + }); + }); + }; + + return ( + <> +
+ + +
+ {buttons.length > 0 && } + {options.length > 0 && } + + ); +}; + +export default ChatMessage; diff --git a/GUI/src/components/HistoricalChat/HistoricalChat.scss b/GUI/src/components/HistoricalChat/HistoricalChat.scss new file mode 100644 index 00000000..94c4dce2 --- /dev/null +++ b/GUI/src/components/HistoricalChat/HistoricalChat.scss @@ -0,0 +1,225 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.historical-chat { + $self: &; + display: flex; + justify-content: flex-end; + position: relative; + height: 100%; + white-space: pre-wrap; + + &__body { + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + &__header { + padding: get-spacing(haapsalu); + background-color: get-color(extra-light); + border-bottom: 1px solid get-color(black-coral-2); + } + + &__group-wrapper { + flex: 1; + padding: get-spacing(haapsalu) get-spacing(haapsalu) 0; + overflow: auto; + overflow-anchor: none; + + #anchor { + overflow-anchor: auto; + height: 1px; + margin-top: 16px; + } + } + + &__toolbar-row { + border-top: 1px solid get-color(black-coral-2); + padding: get-spacing(paldiski) get-spacing(haapsalu); + } + + &__group { + display: flex; + flex-direction: column; + gap: 4px; + padding-left: 56px; + position: relative; + + &:not(:last-child) { + margin-bottom: 8px; + } + + &--buerokratt, + &--chatbot { + #{$self} { + &__group-initials { + background-color: get-color(sapphire-blue-10); + left: 0; + top: 0; + } + + &__group-name { + color: get-color(sapphire-blue-10); + text-align: left; + margin-left: 0; + font-size: $veera-font-size-100; + } + + &__messages { + display: flex; + flex-direction: column; + gap: 4px; + align-items: flex-start; + } + + &__message-text { + background-color: get-color(sapphire-blue-10); + color: get-color(white); + margin-left: 0; + + :any-link { + color: get-color(white); + } + + &:hover { + background-color: get-color(sapphire-blue-13); + } + } + } + } + + &--backoffice-user { + #{$self} { + &__group-initials { + background-color: get-color(black-coral-10); + color: get-color(white); + left: 0; + } + + &__group-name { + text-align: left; + margin-left: 0; + font-size: $veera-font-size-100; + } + + &__messages { + display: flex; + flex-direction: column; + gap: 4px; + align-items: flex-start; + } + + &__message-text { + background-color: get-color(black-coral-10); + color: get-color(white); + margin-left: 0; + + :any-link { + color: get-color(white); + } + + &:hover { + background-color: #535665; + } + } + } + } + } + + &__group-initials { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background-color: get-color(black-coral-1); + border-radius: 50%; + font-size: $veera-font-size-80; + line-height: $veera-line-height-500; + font-weight: $veera-font-weight-delta; + color: get-color(black-coral-6); + position: absolute; + right: 0; + top: 0; + } + + &__group-name { + font-size: $veera-font-size-100; + line-height: $veera-line-height-500; + font-weight: $veera-font-weight-delta; + text-transform: capitalize; + margin-left: 10rem; + margin-right: 3rem; + text-align: right; + padding-top: 7px; + } + + &__messages { + display: flex; + flex-direction: column; + gap: 4px; + align-items: flex-end; + } + + &__message { + display: flex; + align-items: center; + gap: 8px; + } + + &__message-text { + display: flex; + align-items: center; + padding: 4px 8px; + background-color: get-color(black-coral-0); + border-radius: 8px 8px 8px 4px; + font-size: $veera-font-size-100; + line-height: $veera-line-height-500; + transition: all .25s ease-out; + cursor: pointer; + + :any-link { + text-decoration: underline; + } + + &:hover { + background-color: get-color(black-coral-1); + } + } + + &__message-date { + color: get-color(black-coral-6); + font-size: 11px; + line-height: 20px; + } + + &__comment-text { + color: get-color(black); + + &.placeholder { + color: get-color(black-coral-6); + } + } +} + +.header-link { + text-decoration: none; + padding: 15px 35px; + font-family: Roboto , sans-serif; + font-weight: 400; +} + +.title { + font-size: 14px; + color: get-color(black-coral-6); + padding-top: 4px; +} \ No newline at end of file diff --git a/GUI/src/components/HistoricalChat/index.tsx b/GUI/src/components/HistoricalChat/index.tsx new file mode 100644 index 00000000..6eed87b7 --- /dev/null +++ b/GUI/src/components/HistoricalChat/index.tsx @@ -0,0 +1,203 @@ +import {FC, useEffect, useRef, useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import clsx from 'clsx'; +import {ReactComponent as BykLogoWhite} from '../../assets/logo-white.svg'; +import {BACKOFFICE_NAME, Chat as ChatType, CHAT_EVENTS} from '../../types/chat'; +import {Message} from '../../types/message'; +import ChatMessage from './ChatMessage'; +import './HistoricalChat.scss'; +import {analyticsApi} from '../services/api'; +import ChatEvent from '../ChatEvent'; +import {AUTHOR_ROLES} from '../../util/constants'; + +type ChatProps = { + chat: ChatType; + header_link?: string; + trigger: boolean; + onChatStatusChange: (event: string) => void; + onCommentChange: (comment: string) => void; + selectedStatus: string | null; +}; + +type GroupedMessage = { + name: string; + title: string; + type: string; + messages: Message[]; +}; + +const chatStatuses = [ + CHAT_EVENTS.ACCEPTED, + CHAT_EVENTS.CLIENT_LEFT_FOR_UNKNOWN_REASONS, + CHAT_EVENTS.CLIENT_LEFT_WITH_ACCEPTED, + CHAT_EVENTS.CLIENT_LEFT_WITH_NO_RESOLUTION, + CHAT_EVENTS.HATE_SPEECH, + CHAT_EVENTS.OTHER, + CHAT_EVENTS.RESPONSE_SENT_TO_CLIENT_EMAIL, +]; + +const HistoricalChat: FC = ({ + chat, + header_link, + trigger, + selectedStatus, + onChatStatusChange, + onCommentChange, + }) => { + const {t} = useTranslation(); + const chatRef = useRef(null); + const [messageGroups, setMessageGroups] = useState([]); + const [messagesList, setMessagesList] = useState([]); + + useEffect(() => { + const initializeComponent = () => { + setMessageGroups([]); + getMessages(); + }; + + initializeComponent(); + }, []); + + useEffect(() => { + getMessages(); + }, [chat]); + + const getMessages = async () => { + if (!chat.id) return; + const {data: res} = await analyticsApi.post('agents/chats/messages/all', { + chatId: chat.id, + }); + setMessagesList(res.response); + }; + + const endUserFullName = + chat.endUserFirstName !== '' && chat.endUserLastName !== '' + ? `${chat.endUserFirstName} ${chat.endUserLastName}` + : t('global.anonymous'); + + useEffect(() => { + if (!messagesList) return; + let groupedMessages: GroupedMessage[] = []; + messagesList.forEach((message) => { + const lastGroup = groupedMessages[groupedMessages.length - 1]; + if ( + lastGroup && + lastGroup.type === AUTHOR_ROLES.BACKOFFICE_USER && + lastGroup.messages.at(-1) && + message.event === CHAT_EVENTS.READ + ) { + lastGroup.messages.push(message); + return; + } + if (lastGroup?.type === message.authorRole) { + if ( + !message.event || + message.event.toLowerCase() === CHAT_EVENTS.GREETING || + message.event.toLowerCase() === CHAT_EVENTS.WAITING_VALIDATION || + message.event.toLowerCase() === CHAT_EVENTS.APPROVED_VALIDATION + ) { + lastGroup.messages.push({ + ...message, + content: + message.event === CHAT_EVENTS.WAITING_VALIDATION + ? t('chat.waiting_validation').toString() + : message.content, + }); + } else { + groupedMessages.push({ + name: '', + type: 'event', + title: '', + messages: [{...message}], + }); + } + } else { + const isBackOfficeUser = message.authorRole === 'backoffice-user' + ? `${message.authorFirstName} ${message.authorLastName}` + : BACKOFFICE_NAME.DEFAULT; + groupedMessages.push({ + name: + message.authorRole === 'end-user' + ? endUserFullName + : isBackOfficeUser, + type: message.authorRole, + title: message.csaTitle ?? '', + messages: [{...message}], + }); + } + }); + + setMessageGroups(groupedMessages); + }, [messagesList, endUserFullName]); + + useEffect(() => { + if (!chatRef.current || !messageGroups) return; + chatRef.current.scrollIntoView({block: 'end', inline: 'end'}); + }, [messageGroups]); + + const isEvent = (group: GroupedMessage) => { + return ( + group.type === 'event' || + group.name.trim() === '' || + (!group.messages[0].content && group.messages[0].event) + ); + }; + + return ( +
+
+ {header_link && ( +
{header_link}
+ )} +
+ {messageGroups?.map((group, index) => ( +
+ {isEvent(group) ? ( + + ) : ( + <> +
+ {group.type === 'buerokratt' || group.type === 'chatbot' ? ( + + ) : ( + <> + {group.name + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase()} + + )} +
+
+ {group.name} + { + group.title.length > 0 && ( +
{group.title}
+ ) + } +
+
+ {group.messages.map((message, i) => ( + + ))} +
+ + )} +
+ ))} +
+
+
+
+
+ ); +}; + +export default HistoricalChat; diff --git a/GUI/src/i18n/en/common.json b/GUI/src/i18n/en/common.json index a106e0cd..3a929bc2 100644 --- a/GUI/src/i18n/en/common.json +++ b/GUI/src/i18n/en/common.json @@ -267,9 +267,180 @@ "avg_chat_time_csa": "Conversation time by advisor" }, "chat": { - "forwarded": "Forwarded", + "userTyping": "User Typing", + "chatForwardedTo": "Chat forwarded to", + "chatEnded": "Chat ended", + "reply": "Reply", + "unansweredChats": "Unanswered chats", "unanswered": "Unanswered", - "pending": "Pending" + "forwarded": "Forwarded", + "pending": "Pending", + "endUser": "End user name", + "endUserId": "End user id", + "csaName": "Client support name", + "endUserEmail": "End User Email", + "endUserPhoneNumber": "End User Phone", + "startedAt": "Chat started at", + "device": "Device", + "location": "Location", + "redirectedMessageByOwner": "{{from}} forwarded chat to {{to}} {{date}}", + "redirectedMessageClaimed": "{{to}} took over chat from {{from}} {{date}}", + "redirectedMessage": "{{user}} forwarded chat from {{from}} to {{to}} {{date}}", + "new": "New", + "inProcess": "In Process", + "status": { + "active": "Active", + "ended": "Unspecified" + }, + "chatStatus": "Chat status", + "changeStatus": "Change status", + "waiting_validation": "Waiting for validation", + "approved_validation": "Approved Validation", + "active": { + "list": "Active chat list", + "myChats": "My chats", + "newChats": "New Chats", + "chooseChat": "Choose a chat to begin", + "endChat": "End chat", + "takeOver": "Take Over", + "askAuthentication": "Ask for authentication", + "askForContact": "Ask for contact", + "askPermission": "Ask permission", + "forwardToColleague": "Forward to colleague", + "forwardToOrganization": "Forward to organization", + "startedAt": "Chat started at {{date}}", + "forwardChat": "Who to forward the chat?", + "searchByName": "Search by name", + "onlyActiveAgents": "Show only active client support agents", + "establishment": "Establishment", + "searchByEstablishmentName": "Search by establishment name", + "sendToEmail": "Send to email", + "chooseChatStatus": "Choose chat status", + "statusChange": "Chat status change", + "startService": "Start a service", + "selectService": "Select a service", + "start": "Start", + "service": "Service", + "ContactedUser": "Contacted User", + "couldNotReachUser": "Could Not Reach User" + }, + "history": { + "title": "History", + "searchChats": "Search chats", + "startTime": "Start time", + "endTime": "End time", + "csaName": "Customer support name", + "contact": "Contact", + "comment": "Comment", + "label": "Label", + "nps": "NPS", + "forwarded": "Forwarded", + "addACommentToTheConversation": "Add a comment to the conversation", + "rating": "Rating", + "feedback": "Feedback" + }, + "plainEvents": { + "answered": "Answered", + "terminated": "Unspecified", + "sent_to_csa_email": "Chat sent to CSA email", + "client-left": "Client left", + "client_left_with_accepted": "Client left with accepted response", + "client_left_with_no_resolution": "Client left with no resolution", + "client_left_for_unknown_reasons": "Client left for unknown reason", + "accepted": "Accepted response", + "hate_speech": "Hate speech", + "other": "Other reasons", + "response_sent_to_client_email": "Response was sent to client email", + "greeting": "Greetings", + "requested-authentication": "Requested authentication", + "authentication_successful": "Authentication successful", + "authentication_failed": "Authentication failed", + "ask-permission": "Asked permission", + "ask-permission-accepted": "Permission accepted", + "ask-permission-rejected": "Permission rejected", + "ask-permission-ignored": "Permission ignored", + "rating": "Rating", + "contact-information": "Requested contact information", + "contact-information-rejected": "Contact information rejected", + "contact-information-fulfilled": "Contact information fulfilled", + "requested-chat-forward": "Requested chat forward", + "requested-chat-forward-accepted": "Requested chat forward accepted", + "requested-chat-forward-rejected": "Requested chat forward rejected", + "inactive-chat-ended": "Ended cue to inactivity", + "contact-information-skipped": "Contact Information Skipped", + "unavailable-contact-information-fulfilled": "Contact information provided", + "unavailable_organization": "Organization is unavailable", + "unavailable_organization_ask_contacts": "Organization is unavailable and asked for contacts", + "unavailable_csas": "Advisors are not available", + "unavailable_csas_ask_contacts": "Advisors are not available and asked for contacts", + "unavailable_holiday": "Holiday", + "unavailable_holiday_ask_contacts": "Holiday and asked for contacts", + "message-read": "Read", + "user-reached": "User contacted", + "user-not-reached": "User could not be reached", + "ask_to_forward_to_csa": "Asked to forward the conversation to a customer service agent", + "forwarded_to_backoffice": "Conversation forwarded to back office", + "continue_chatting_with_bot": "Continue conversation with bürokratt" + }, + "events": { + "answered": "Answered {{date}}", + "terminated": "Unspecified {{date}}", + "sent_to_csa_email": "Chat sent to CSA email {{date}}", + "client-left": "Client left {{date}}", + "client_left_with_accepted": "Client left with accepted response {{date}}", + "client_left_with_no_resolution": "Client left with no resolution {{date}}", + "client_left_for_unknown_reasons": "Client left for unknown reason {{date}}", + "accepted": "Accepted response {{date}}", + "hate_speech": "Hate speech {{date}}", + "other": "Other reasons {{date}}", + "response_sent_to_client_email": "Response was sent to client email {{date}}", + "greeting": "Greetings {{date}}", + "requested-authentication": "Requested authentication {{date}}", + "authentication_successful": "Authentication successful {{date}}", + "authentication_failed": "Authentication failed {{date}}", + "ask-permission": "Asked permission {{date}}", + "ask-permission-accepted": "Permission accepted {{date}}", + "ask-permission-rejected": "Permission rejected {{date}}", + "ask-permission-ignored": "Permission ignored {{date}}", + "rating": "Rating {{date}}", + "contact-information": "Requested contact information {{date}}", + "contact-information-rejected": "Contact information rejected {{date}}", + "contact-information-fulfilled": "Contact information fulfilled {{date}}", + "requested-chat-forward": "Requested chat forward {{date}}", + "requested-chat-forward-accepted": "Requested chat forward accepted {{date}}", + "requested-chat-forward-rejected": "Requested chat forward rejected {{date}}", + "message-read": "Read", + "contact-information-skipped": "Contact Information Skipped", + "unavailable-contact-information-fulfilled": "Contact information provided", + "unavailable_organization": "Organization is unavailable", + "unavailable_organization_ask_contacts": "Organization is unavailable and asked for contacts", + "unavailable_csas": "Advisors are not available", + "unavailable_csas_ask_contacts": "Advisors are not available and asked for contacts", + "unavailable_holiday": "Holiday", + "unavailable_holiday_ask_contacts": "Holiday and asked for contacts", + "pending-assigned": "{{name}} assigned to contact user", + "user-reached": "{{name}} contacted the user", + "user-not-reached": "{{name}} could not reach the user", + "user-authenticated": "{{name}} is authenticated {{date}}", + "taken-over": "A human took the conversation over", + "ask_to_forward_to_csa": "Asked to forward the conversation to a customer service agent", + "forwarded_to_backoffice": "Conversation forwarded to back office", + "continue_chatting_with_bot": "Continue conversation with bürokratt" + }, + "validations": { + "title": "Validations", + "description": "Responses from the chatbot that require validation before being sent to the end user are displayed here", + "messageChanged": "Message changed successfully", + "messageApproved": "Message approved successfully", + "messageChangeFailed": "Message change failed", + "header": { + "id" : "Id", + "chatId": "Chat Id", + "message": "Message", + "requestedAt": "Requested At", + "approve": "Approve" + } + } }, "settings": { "title": "Settings", diff --git a/GUI/src/i18n/et/common.json b/GUI/src/i18n/et/common.json index 2183f715..55f054ec 100644 --- a/GUI/src/i18n/et/common.json +++ b/GUI/src/i18n/et/common.json @@ -307,9 +307,180 @@ "ROLE_UNAUTHENTICATED": "Autentimata" }, "chat": { + "userTyping": "Kasutaja kirjutab", + "chatForwardedTo": "Vestlus edastati aadressile", + "chatEnded": "Vestlus lõppes", + "reply": "Vasta", + "unansweredChats": "Vastamata vestlused", "unanswered": "Vastamata", "forwarded": "Suunatud", - "pending": "Ootel" + "pending": "Ootel", + "endUser": "Vestleja nimi", + "endUserId": "Vestleja isikukood", + "csaName": "Nõustaja nimi", + "endUserEmail": "Vestleja e-post", + "endUserPhoneNumber": "Vestleja telefoninumber", + "startedAt": "Vestlus alustatud", + "device": "Seade", + "location": "Lähtekoht", + "redirectedMessageByOwner": "{{from}} suunas vestluse kasutajale {{to}} {{date}}", + "redirectedMessageClaimed": "{{to}} võttis vestluse üle kasutajalt {{from}} {{date}}", + "redirectedMessage": "{{user}} suunas vestluse kasutajalt {{from}} kasutajale {{to}} {{date}}", + "new": "uued", + "inProcess": "töös", + "status": { + "active": "Aktiivne", + "ended": "Määramata" + }, + "chatStatus": "Vestluse staatus", + "changeStatus": "Muuda staatust", + "waiting_validation": "Vastust koostatakse", + "approved_validation": "Kinnitatud valideerimine", + "active": { + "list": "Aktiivsed vestlused", + "myChats": "Minu vestlused", + "newChats": "Uued vestlused", + "chooseChat": "Alustamiseks vali vestlus", + "endChat": "Lõpeta vestlus", + "takeOver": "Võta üle", + "askAuthentication": "Küsi autentimist", + "askForContact": "Küsi kontaktandmeid", + "askPermission": "Küsi nõusolekut", + "forwardToColleague": "Suuna kolleegile", + "forwardToOrganization": "Suuna asutusele", + "startedAt": "Vestlus alustatud {{date}}", + "forwardChat": "Kellele vestlus suunata?", + "searchByName": "Otsi nime või tiitli järgi", + "onlyActiveAgents": "Näita ainult kohal olevaid nõustajaid", + "establishment": "Asutus", + "searchByEstablishmentName": "Otsi asutuse nime järgi", + "sendToEmail": "Saada e-postile", + "chooseChatStatus": "Vali vestluse staatus", + "statusChange": "Vestluse staatus", + "startService": "Alusta teenust", + "selectService": "Vali teenus", + "start": "Alusta", + "service": "Teenus", + "ContactedUser": "Kasutajaga ühendust võetud", + "couldNotReachUser": "Kasutajaga ei õnnestunud ühendust saada" + }, + "history": { + "title": "Ajalugu", + "searchChats": "Otsi üle vestluste", + "startTime": "Algusaeg", + "endTime": "Lõppaeg", + "csaName": "Nõustaja nimi", + "contact": "Kontaktandmed", + "comment": "Kommentaar", + "label": "Märksõna", + "nps": "Soovitusindeks", + "forwarded": "Suunatud", + "addACommentToTheConversation": "Lisa vestlusele kommentaar", + "rating": "Hinnang", + "feedback": "Tagasiside" + }, + "plainEvents": { + "answered": "Vastatud", + "terminated": "Määramata", + "sent_to_csa_email": "Vestlus saadetud nõustaja e-postile", + "client-left": "Klient lahkus", + "client_left_with_accepted": "Klient lahkus aktsepteeritud vastusega", + "client_left_with_no_resolution": "Klient lahkus vastuseta", + "client_left_for_unknown_reasons": "Klient lahkus määramata põhjustel", + "accepted": "Aktsepteeritud", + "hate_speech": "Vihakõne", + "other": "Muud põhjused", + "response_sent_to_client_email": "Kliendile vastati tema jäetud kontaktile", + "greeting": "Tervitus", + "requested-authentication": "Autentimine algatatud", + "authentication_successful": "Autentimine õnnestus", + "authentication_failed": "Autentimine ebaõnnestus", + "ask-permission": "Küsiti nõusolekut", + "ask-permission-accepted": "Nõusolek antud", + "ask-permission-rejected": "Nõusolekust keelduti", + "ask-permission-ignored": "Nõusolek ignoreeritud", + "rating": "Hinnang", + "contact-information": "Küsiti kontaktandmeid", + "contact-information-rejected": "Kontaktandmetest keeldutud", + "contact-information-fulfilled": "Kontaktandmed saadetud", + "requested-chat-forward": "Küsiti vestluse suunamist", + "requested-chat-forward-accepted": "Vestluse suunamine aktsepteeritud", + "requested-chat-forward-rejected": "Vestluse suunamine tagasi lükatud", + "inactive-chat-ended": "Lõpetatud tegevusetuse tõttu", + "message-read": "Loetud", + "contact-information-skipped": "Kontaktandmeid ei saadetud", + "unavailable-contact-information-fulfilled": "Kontaktandmed on antud", + "unavailable_organization": "Asutus ei ole kättesaadav", + "unavailable_organization_ask_contacts": "Asutus ei ole kättesaadav ja küsitakse kontakte", + "unavailable_csas": "Klienditeenindajad ei ole kättesaadavad", + "unavailable_csas_ask_contacts": "Klienditeenindajad ei ole kättesaadavad ja küsitakse kontakte", + "unavailable_holiday": "Puhkus", + "unavailable_holiday_ask_contacts": "Puhkus ja küsitakse kontakte", + "user-reached": "Kasutajaga võeti ühendust", + "user-not-reached": "Kasutajaga ei õnnestunud ühendust saada", + "ask_to_forward_to_csa": "Vestlus paluti klienditeenindajale suunata", + "forwarded_to_backoffice": "Vestlus suunatakse klienditeenindajale", + "continue_chatting_with_bot": "Jätkake vestlust kasutajaga bürokratt" + }, + "events": { + "answered": "Vastatud {{date}}", + "terminated": "Määramata {{date}}", + "sent_to_csa_email": "Vestlus saadetud nõustaja e-postile {{date}}", + "client-left": "Klient lahkus {{date}}", + "client_left_with_accepted": "Klient lahkus aktsepteeritud vastusega {{date}}", + "client_left_with_no_resolution": "Klient lahkus vastuseta {{date}}", + "client_left_for_unknown_reasons": "Klient lahkus määramata põhjustel {{date}}", + "accepted": "Aktsepteeritud {{date}}", + "hate_speech": "Vihakõne {{date}}", + "other": "Muud põhjused {{date}}", + "response_sent_to_client_email": "Kliendile vastati tema jäetud kontaktile {{date}}", + "greeting": "Tervitus", + "requested-authentication": "Küsiti autentimist", + "authentication_successful": "Autentimine õnnestus {{date}}", + "authentication_failed": "Autentimine ebaõnnestus {{date}}", + "ask-permission": "Küsiti nõusolekut", + "ask-permission-accepted": "Nõusolek antud {{date}}", + "ask-permission-rejected": "Nõusolekust keeldutud {{date}}", + "ask-permission-ignored": "Nõusolek ignoreeritud {{date}}", + "rating": "Hinnang {{date}}", + "contact-information": "Küsiti kontaktandmeid {{date}}", + "contact-information-rejected": "Kontaktandmetest keeldutud {{date}}", + "contact-information-fulfilled": "Kontaktandmed saadetud {{date}}", + "requested-chat-forward": "Küsiti vestluse suunamist", + "requested-chat-forward-accepted": "Vestluse suunamine aktsepteeritud {{date}}", + "requested-chat-forward-rejected": "Vestluse suunamine tagasi lükatud {{date}}", + "message-read": "Loetud", + "contact-information-skipped": "Kontaktandmeid ei saadetud", + "unavailable-contact-information-fulfilled": "Kontaktandmed on antud", + "unavailable_organization": "Asutus ei ole kättesaadav", + "unavailable_organization_ask_contacts": "Asutus ei ole kättesaadav ja küsitakse kontakte", + "unavailable_csas": "Klienditeenindajad ei ole kättesaadavad", + "unavailable_csas_ask_contacts": "Klienditeenindajad ei ole kättesaadavad ja küsitakse kontakte", + "unavailable_holiday": "Puhkus", + "unavailable_holiday_ask_contacts": "Puhkus ja küsitakse kontakte", + "pending-assigned": "{{name}} määratud kontaktkasutajale", + "user-reached": "{{name}} võttis kasutajaga ühendust", + "user-not-reached": "{{name}} ei saanud kasutajaga ühendust", + "user-authenticated": "{{name}} on autenditud {{date}}", + "taken-over": "Klienditeenindaja võttis vestluse üle", + "ask_to_forward_to_csa": "Vestlus paluti klienditeenindajale suunata", + "forwarded_to_backoffice": "Vestlus suunatakse klienditeenindajale", + "continue_chatting_with_bot": "Jätkake vestlust kasutajaga bürokratt" + }, + "validations": { + "title": "Valideerimised", + "description": "Siin kuvatakse vestlusroboti poolt tulnud vastused, mis nõuavad valideerimist enne kui need lõppkasutajale saadetakse", + "messageChanged": "Sõnumi muutmine õnnestus", + "messageApproved": "Sõnum kinnitati edukalt", + "messageChangeFailed": "Sõnumi muutmine ebaõnnestus", + "header": { + "id": "Id", + "chatId": "Vestluse ID", + "message": "Sõnum", + "requestedAt": "Taotletud Kell", + "approve": "Kinnita" + } + } }, "units": { "chats": "vestlused", diff --git a/GUI/tsconfig.json b/GUI/tsconfig.json index 16fff78a..9442c1b6 100644 --- a/GUI/tsconfig.json +++ b/GUI/tsconfig.json @@ -1,24 +1,29 @@ { "compilerOptions": { - "target": "es5", + "target": "ESNext", + "useDefineForClassFields": true, "lib": [ - "dom", - "dom.iterable", - "esnext" + "DOM", + "DOM.Iterable", + "ESNext" ], - "allowJs": true, + "allowJs": false, "skipLibCheck": true, - "esModuleInterop": true, + "esModuleInterop": false, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "module": "esnext", - "moduleResolution": "node", + "module": "ESNext", + "moduleResolution": "Node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "baseUrl": "src", + "types": [ + "vite/client", + "node" + ] }, "include": [ "src" From b3b08c50113dd37a881a6bc8c88d5e5bd2984f32 Mon Sep 17 00:00:00 2001 From: VassiliMoskaljov Date: Wed, 18 Dec 2024 11:49:10 +0200 Subject: [PATCH 3/4] updated view button logic --- GUI/src/components/ChatsTable/index.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/GUI/src/components/ChatsTable/index.tsx b/GUI/src/components/ChatsTable/index.tsx index a747f6a3..9cabc465 100644 --- a/GUI/src/components/ChatsTable/index.tsx +++ b/GUI/src/components/ChatsTable/index.tsx @@ -41,16 +41,10 @@ const ChatsTable = (props: Props) => { const dateTimeFormat = (props: CellContext) => formatDate(new Date(props.getValue()), 'd. MMM yyyy HH:mm:ss'); - - const fillChatData = (data: any) => { - getChatById.mutate(data.row.original.baseId ?? ''); - setSelectedChat(selectedChat) - } - const feedbackViewButton = (dataTableProps: any) => (