@buerokratt-ria/common-gui-components 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.eslintrc.json +18 -0
  2. package/CHANGELOG.md +7 -0
  3. package/MAKING_CHANGES.md +8 -0
  4. package/README.md +49 -0
  5. package/assets/ding.mp3 +0 -0
  6. package/assets/logo-white.svg +29 -0
  7. package/assets/logo.svg +31 -0
  8. package/assets/newMessageSound.mp3 +0 -0
  9. package/constants/config.ts +12 -0
  10. package/constants/index.ts +1 -0
  11. package/context/index.ts +1 -0
  12. package/context/toastContext.tsx +60 -0
  13. package/hooks/index.ts +3 -0
  14. package/hooks/useAudio.tsx +30 -0
  15. package/hooks/useDocumentEscapeListener.tsx +17 -0
  16. package/hooks/useToast.tsx +5 -0
  17. package/i18n.ts +26 -0
  18. package/index.ts +6 -0
  19. package/package.json +122 -0
  20. package/project.json +52 -0
  21. package/services/api.ts +74 -0
  22. package/services/index.ts +3 -0
  23. package/services/sse-service.ts +30 -0
  24. package/services/users.ts +58 -0
  25. package/store/index.ts +253 -0
  26. package/templates/history-page/index.ts +1 -0
  27. package/templates/history-page/src/History.scss +47 -0
  28. package/templates/history-page/src/index.tsx +998 -0
  29. package/templates/history-page/src/unfiyDate.tsx +7 -0
  30. package/translations/en/common.json +467 -0
  31. package/translations/et/common.json +467 -0
  32. package/tsconfig.base.json +21 -0
  33. package/tsconfig.json +17 -0
  34. package/tsconfig.spec.json +19 -0
  35. package/types/authorities.ts +8 -0
  36. package/types/botConfig.ts +7 -0
  37. package/types/chat.ts +126 -0
  38. package/types/customerSupportActivity.ts +5 -0
  39. package/types/deleteChatSettings.ts +9 -0
  40. package/types/emergencyNotice.ts +10 -0
  41. package/types/establishment.ts +4 -0
  42. package/types/index.ts +18 -0
  43. package/types/mainNavigation.ts +11 -0
  44. package/types/message.ts +74 -0
  45. package/types/organizationWorkingTime.ts +27 -0
  46. package/types/router.ts +4 -0
  47. package/types/service.ts +6 -0
  48. package/types/session.ts +7 -0
  49. package/types/skmConfig.ts +8 -0
  50. package/types/user.ts +40 -0
  51. package/types/userInfo.ts +16 -0
  52. package/types/userProfileSettings.ts +10 -0
  53. package/types/widgetConfig.ts +8 -0
  54. package/ui-components/Button/Button.scss +150 -0
  55. package/ui-components/Button/index.tsx +41 -0
  56. package/ui-components/ButtonMessage/ButtonMessage.scss +16 -0
  57. package/ui-components/ButtonMessage/index.tsx +19 -0
  58. package/ui-components/Card/Card.scss +69 -0
  59. package/ui-components/Card/index.tsx +39 -0
  60. package/ui-components/Chat/Chat.scss +447 -0
  61. package/ui-components/Chat/ChatMessage.tsx +270 -0
  62. package/ui-components/Chat/ChatTextArea.scss +110 -0
  63. package/ui-components/Chat/ChatTextArea.tsx +97 -0
  64. package/ui-components/Chat/LoaderOverlay.tsx +39 -0
  65. package/ui-components/Chat/Markdownify.tsx +49 -0
  66. package/ui-components/Chat/PreviewMessage.tsx +39 -0
  67. package/ui-components/Chat/Typing.scss +46 -0
  68. package/ui-components/Chat/index.tsx +1111 -0
  69. package/ui-components/ChatEvent/Chat.scss +40 -0
  70. package/ui-components/ChatEvent/index.tsx +216 -0
  71. package/ui-components/DataTable/CloseIcon.tsx +22 -0
  72. package/ui-components/DataTable/DataTable.scss +188 -0
  73. package/ui-components/DataTable/DeboucedInput.scss +11 -0
  74. package/ui-components/DataTable/DebouncedInput.tsx +54 -0
  75. package/ui-components/DataTable/Filter.tsx +121 -0
  76. package/ui-components/DataTable/index.tsx +432 -0
  77. package/ui-components/Dialog/Dialog.scss +63 -0
  78. package/ui-components/Dialog/index.tsx +44 -0
  79. package/ui-components/Drawer/Drawer.scss +40 -0
  80. package/ui-components/Drawer/index.tsx +42 -0
  81. package/ui-components/FormElements/FormCheckbox/FormCheckbox.scss +57 -0
  82. package/ui-components/FormElements/FormCheckbox/index.tsx +39 -0
  83. package/ui-components/FormElements/FormCheckboxes/FormCheckboxes.scss +63 -0
  84. package/ui-components/FormElements/FormCheckboxes/index.tsx +44 -0
  85. package/ui-components/FormElements/FormDatepicker/FormDatepicker.scss +154 -0
  86. package/ui-components/FormElements/FormDatepicker/index.tsx +123 -0
  87. package/ui-components/FormElements/FormInput/FormInput.scss +90 -0
  88. package/ui-components/FormElements/FormInput/index.tsx +47 -0
  89. package/ui-components/FormElements/FormRadios/FormRadios.scss +72 -0
  90. package/ui-components/FormElements/FormRadios/index.tsx +36 -0
  91. package/ui-components/FormElements/FormSelect/FormMultiselect.tsx +124 -0
  92. package/ui-components/FormElements/FormSelect/FormSelect.scss +121 -0
  93. package/ui-components/FormElements/FormSelect/index.tsx +100 -0
  94. package/ui-components/FormElements/FormTextarea/FormTextarea.scss +109 -0
  95. package/ui-components/FormElements/FormTextarea/index.tsx +154 -0
  96. package/ui-components/FormElements/Switch/Switch.scss +69 -0
  97. package/ui-components/FormElements/Switch/index.tsx +65 -0
  98. package/ui-components/FormElements/SwitchBox/SwitchBox.scss +45 -0
  99. package/ui-components/FormElements/SwitchBox/index.tsx +44 -0
  100. package/ui-components/FormElements/index.tsx +23 -0
  101. package/ui-components/HistoricalChat/ChatMessage.tsx +67 -0
  102. package/ui-components/HistoricalChat/HistoricalChat.scss +225 -0
  103. package/ui-components/HistoricalChat/index.tsx +282 -0
  104. package/ui-components/Icon/Icon.scss +17 -0
  105. package/ui-components/Icon/index.tsx +26 -0
  106. package/ui-components/Label/Label.scss +76 -0
  107. package/ui-components/Label/index.tsx +40 -0
  108. package/ui-components/OptionMessage/OptionMessage.scss +16 -0
  109. package/ui-components/OptionMessage/index.tsx +16 -0
  110. package/ui-components/Toast/Toast.scss +73 -0
  111. package/ui-components/Toast/index.tsx +54 -0
  112. package/ui-components/Tooltip/Tooltip.scss +17 -0
  113. package/ui-components/Tooltip/index.tsx +28 -0
  114. package/ui-components/Track/index.tsx +57 -0
  115. package/ui-components/index.tsx +53 -0
  116. package/utils/constants.ts +19 -0
  117. package/utils/format-bytes.ts +8 -0
  118. package/utils/generateUEID.ts +8 -0
  119. package/utils/local-storage-utils.ts +17 -0
  120. package/utils/parse-utils.ts +23 -0
  121. package/utils/state-management-utils.ts +13 -0
  122. package/vite.config.ts +67 -0
@@ -0,0 +1,1111 @@
1
+ import { ChangeEvent, FC, useEffect, useRef, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { format } from 'date-fns';
4
+ import { et } from 'date-fns/locale';
5
+ import clsx from 'clsx';
6
+ import {
7
+ MdClose,
8
+ MdDoneOutline,
9
+ MdOutlineAttachFile,
10
+ MdOutlineCreate,
11
+ MdOutlineSend,
12
+ } from 'react-icons/md';
13
+ import { Button, Icon, Label, Track } from '../';
14
+ import { ReactComponent as BykLogoWhite } from '../../assets/logo-white.svg';
15
+ import {
16
+ BACKOFFICE_NAME,
17
+ Chat as ChatType,
18
+ CHAT_EVENTS,
19
+ CHAT_STATUS,
20
+ } from '../../types/chat';
21
+ import { useMutation, useQuery } from '@tanstack/react-query';
22
+ import { AttachmentTypes, Message } from '../../types/message';
23
+ import ChatMessage from './ChatMessage';
24
+ import ChatEvent from '../ChatEvent';
25
+ import { CHAT_INPUT_LENGTH, isHiddenFeaturesEnabled } from '../../constants';
26
+ import { apiDev } from '../../services';
27
+ import ChatTextArea from './ChatTextArea';
28
+ import { AUTHOR_ROLES, MESSAGE_FILE_SIZE_LIMIT, ROLES } from '../../utils/constants';
29
+ import { AxiosError } from 'axios';
30
+ import { useToast, useNewMessageSound } from '../../hooks';
31
+ import useStore from '../../store';
32
+ import { userStore as useHeaderStore } from '@buerokratt-ria/header';
33
+ import sse from '../../services/sse-service';
34
+ import { useNavigate } from 'react-router-dom';
35
+ import PreviewMessage from './PreviewMessage';
36
+ import LoaderOverlay from './LoaderOverlay';
37
+ import './Chat.scss';
38
+ import { useInterval } from 'usehooks-ts';
39
+ import { BotConfig } from '../../types/botConfig';
40
+
41
+ type ChatProps = {
42
+ chat: ChatType;
43
+ isCsaNameVisible: boolean;
44
+ isCsaTitleVisible: boolean;
45
+ onChatEnd: (chat: ChatType) => void;
46
+ onForwardToColleauge?: (chat: ChatType) => void;
47
+ onForwardToEstablishment?: (chat: ChatType) => void;
48
+ onSendToEmail?: (chat: ChatType) => void;
49
+ onStartAService?: (chat: ChatType) => void;
50
+ onRefresh: () => void;
51
+ };
52
+
53
+ type GroupedMessage = {
54
+ name: string;
55
+ type: string;
56
+ title: string;
57
+ messages: Message[];
58
+ };
59
+
60
+ const Chat: FC<ChatProps> = ({
61
+ chat,
62
+ isCsaNameVisible,
63
+ isCsaTitleVisible,
64
+ onChatEnd,
65
+ onForwardToColleauge,
66
+ onForwardToEstablishment,
67
+ onSendToEmail,
68
+ onStartAService,
69
+ onRefresh,
70
+ }) => {
71
+ const { t } = useTranslation();
72
+ const userInfo = useStore((state) => state.userInfo);
73
+ const chatRef = useRef<HTMLDivElement>(null);
74
+
75
+ const [messageGroups, setMessageGroups] = useState<GroupedMessage[]>([]);
76
+ const messageGroupsRef = useRef(messageGroups);
77
+ const setMessageGroupsState = (data: GroupedMessage[]) => {
78
+ messageGroupsRef.current = data;
79
+ setMessageGroups(data);
80
+ };
81
+ const toast = useToast();
82
+
83
+ const [responseText, setResponseText] = useState('');
84
+ const chatCsaActive = useHeaderStore((state) => state.chatCsaActive);
85
+ const [messagesList, setMessagesList] = useState<Message[]>([]);
86
+ const messageListRef = useRef(messagesList);
87
+ const [latestPermissionMessageCreated, setLatestPermissionMessageCreated] =
88
+ useState<string>();
89
+ const [latestPermissionMessageSeconds, setLatestPermissionMessageSeconds] =
90
+ useState<number>(0);
91
+ const [previewTypingMessage, setPreviewTypingMessage] = useState<
92
+ string | undefined
93
+ >();
94
+ const [selectedMessage, setSelectedMessage] = useState<Message | null>(null);
95
+ const [isChatEditingAllowed, setIsChatEditingAllowed] =
96
+ useState<boolean>(false);
97
+
98
+ const [newMessageEffect] = useNewMessageSound();
99
+ const navigate = useNavigate();
100
+
101
+ const askPermissionsTimeoutInSeconds = 60;
102
+ let messagesLength = 0;
103
+
104
+ const calculatePermissionMessageSeconds = () => {
105
+ if (latestPermissionMessageCreated) {
106
+ const countdown =
107
+ Math.round(
108
+ (new Date().getTime() -
109
+ new Date(latestPermissionMessageCreated).getTime()) /
110
+ 1000
111
+ ) ?? 0;
112
+
113
+ setLatestPermissionMessageSeconds(countdown);
114
+ }
115
+ };
116
+
117
+ const handlePermissionMessages = () => {
118
+ const permissionsMessages = messagesList.filter(
119
+ (e: Message) =>
120
+ e.event === 'ask-permission' ||
121
+ e.event === 'ask-permission-accepted' ||
122
+ e.event === 'ask-permission-rejected' ||
123
+ e.event === 'ask-permission-ignored'
124
+ );
125
+
126
+ setLatestPermissionMessageCreated(
127
+ permissionsMessages[permissionsMessages.length - 1]?.created ?? ''
128
+ );
129
+ calculatePermissionMessageSeconds();
130
+ };
131
+
132
+ useInterval(
133
+ () => {
134
+ calculatePermissionMessageSeconds();
135
+ },
136
+ latestPermissionMessageCreated &&
137
+ latestPermissionMessageSeconds <= askPermissionsTimeoutInSeconds
138
+ ? 1000
139
+ : null
140
+ );
141
+
142
+ useEffect(() => {
143
+ getMessages();
144
+ }, []);
145
+
146
+ useEffect(() => {
147
+ messageListRef.current = messagesList;
148
+ }, [messagesList]);
149
+
150
+ useEffect(() => {
151
+ const onMessage = async (res: any) => {
152
+ if (res.type === 'preview') {
153
+ const previewMessage = await apiDev.get(
154
+ 'agents/chats/messages/preview?chatId=' + chat.id
155
+ );
156
+ setPreviewTypingMessage(previewMessage.data.response);
157
+ } else if (messageListRef.current?.length > 0) {
158
+ const res =
159
+ (await apiDev.get(
160
+ `agents/chats/messages/new?chatId=${chat.id}&lastRead=${
161
+ chat.lastMessageTimestamp?.split('+')[0] ?? ''
162
+ }`
163
+ )) ?? [];
164
+ const messages = res.data.response;
165
+ setPreviewTypingMessage(undefined);
166
+ const filteredMessages = messages?.filter((newMessage: Message) => {
167
+ return filterMessages(messageListRef.current, newMessage);
168
+ });
169
+
170
+ let newDisplayableMessages = filteredMessages?.filter(
171
+ (msg: Message) => msg.authorId != userInfo?.idCode
172
+ );
173
+
174
+ if (newDisplayableMessages?.length > 0) {
175
+ setMessagesList((oldMessages) => [
176
+ ...oldMessages,
177
+ ...newDisplayableMessages,
178
+ ]);
179
+ }
180
+
181
+ handlePermissionMessages();
182
+
183
+ const actionEventTypes = [
184
+ 'ask-permission-accepted',
185
+ 'ask-permission-rejected',
186
+ 'ask-permission-ignored',
187
+ 'contact-information-fulfilled',
188
+ 'contact-information-rejected',
189
+ 'requested-chat-forward',
190
+ 'requested-chat-forward-accepted',
191
+ 'requested-chat-forward-rejected',
192
+ 'pending-assigned',
193
+ 'user-reached',
194
+ 'user-not-reached',
195
+ 'user-authenticated',
196
+ 'authentication-successful',
197
+ 'authentication-failed',
198
+ 'redirectedMessageByOwner',
199
+ 'redirectedMessageClaimed',
200
+ 'redirectedMessage',
201
+ ];
202
+
203
+ const eventMessages: Message[] = filteredMessages?.filter(
204
+ (e: Message) => actionEventTypes.includes(e.event ?? '')
205
+ );
206
+
207
+ if (eventMessages?.length > 0) {
208
+ await getMessages();
209
+ }
210
+ }
211
+ };
212
+
213
+ const events = sse(`/${chat.id}`, onMessage);
214
+
215
+ return () => {
216
+ events.close();
217
+ };
218
+ }, [chat.id]);
219
+
220
+ const getMessages = async () => {
221
+ const { data: res } = await apiDev.post('agents/messages-by-id', {
222
+ chatId: chat.id,
223
+ });
224
+
225
+ if (
226
+ messagesLength != 0 &&
227
+ messagesLength < res.response.length &&
228
+ res.response[res.response.length - 1].authorId != userInfo?.idCode
229
+ ) {
230
+ newMessageEffect?.play();
231
+ onRefresh();
232
+ }
233
+ messagesLength = res.response.length;
234
+
235
+ handlePermissionMessages();
236
+
237
+ setMessagesList(res.response);
238
+ };
239
+
240
+ const hiddenFileInputRef = useRef<HTMLInputElement | null>(null);
241
+
242
+ const handleUploadClick = () => {
243
+ hiddenFileInputRef.current?.click();
244
+ };
245
+
246
+ const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
247
+ if (!e.target.files) {
248
+ return;
249
+ }
250
+ const base64 = await handleFileRead(e.target.files[0]);
251
+
252
+ if (!base64) return;
253
+
254
+ // To be added: file upload logic
255
+ // setUserInput(e.target.files[0].name);
256
+ // setUserInputFile({
257
+ // chatId: chat.id,
258
+ // name: e.target.files[0].name,
259
+ // type: e.target.files[0].type as AttachmentTypes,
260
+ // size: e.target.files[0].size,
261
+ // base64: base64,
262
+ // });
263
+ };
264
+
265
+ const postMessageMutation = useMutation({
266
+ mutationFn: ({
267
+ message,
268
+ editing,
269
+ }: {
270
+ message: Message;
271
+ editing: boolean;
272
+ }) => {
273
+ if (editing) {
274
+ return apiDev.post('agents/chats/messages/edit', message);
275
+ } else {
276
+ return apiDev.post('agents/chats/messages/insert', message);
277
+ }
278
+ },
279
+ onSuccess: (res: any) => {
280
+ return res.data.response;
281
+ },
282
+ onError: (error: AxiosError) => {
283
+ toast.open({
284
+ type: 'error',
285
+ title: t('global.notificationError'),
286
+ message: error.message,
287
+ });
288
+ },
289
+ });
290
+
291
+ const postEventMutation = useMutation({
292
+ mutationFn: (message: Message) =>
293
+ apiDev.post('agents/chats/messages/insert', {
294
+ chatId: message.chatId ?? '',
295
+ content: '',
296
+ event: message.event ?? '',
297
+ authorTimestamp: message.authorTimestamp ?? '',
298
+ }),
299
+ onSuccess: async () => {
300
+ await getMessages();
301
+ },
302
+ onError: (error: AxiosError) => {
303
+ toast.open({
304
+ type: 'error',
305
+ title: t('global.notificationError'),
306
+ message: error.message,
307
+ });
308
+ },
309
+ });
310
+
311
+ const postMessageWithNewEventMutation = useMutation({
312
+ mutationFn: (message: Message) =>
313
+ apiDev.post('agents/chats/messages/event', {
314
+ id: message.id,
315
+ event: CHAT_EVENTS.ASK_PERMISSION,
316
+ authorTimestamp: message.authorTimestamp,
317
+ }),
318
+ onSuccess: async () => {
319
+ await getMessages();
320
+ handleChatEvent(CHAT_EVENTS.ASK_PERMISSION);
321
+ },
322
+ onError: (error: AxiosError) => {
323
+ toast.open({
324
+ type: 'error',
325
+ title: t('global.notificationError'),
326
+ message: error.message,
327
+ });
328
+ },
329
+ });
330
+
331
+ const takeOverChatMutation = useMutation({
332
+ mutationFn: () =>
333
+ apiDev.post('chats/claim', {
334
+ id: chat.id ?? '',
335
+ customerSupportId: userInfo?.idCode ?? '',
336
+ customerSupportDisplayName: userInfo?.displayName ?? '',
337
+ csaTitle: userInfo?.csaTitle ?? '',
338
+ forwardedByUser: userInfo?.idCode ?? '',
339
+ forwardedFromCsa: userInfo?.idCode ?? '',
340
+ forwardedToCsa: userInfo?.idCode ?? '',
341
+ }),
342
+ onSuccess: async () => {
343
+ if (chat.customerSupportId === '') {
344
+ navigate('/active', {
345
+ state: {
346
+ chatId: chat.id,
347
+ },
348
+ });
349
+ } else {
350
+ chat.customerSupportId = userInfo?.idCode;
351
+ }
352
+ onRefresh();
353
+ },
354
+ onError: (error: AxiosError) => {
355
+ toast.open({
356
+ type: 'error',
357
+ title: t('global.notificationError'),
358
+ message: error.message,
359
+ });
360
+ },
361
+ });
362
+
363
+ const assignPendingChatMutation = useMutation({
364
+ mutationFn: () =>
365
+ apiDev.post('chats/pending/assign', {
366
+ id: chat.id ?? '',
367
+ customerSupportId: userInfo?.idCode ?? '',
368
+ customerSupportDisplayName: userInfo?.displayName ?? '',
369
+ csaTitle: userInfo?.csaTitle ?? '',
370
+ }),
371
+ onSuccess: async () => {
372
+ chat.customerSupportId = userInfo?.idCode;
373
+ onRefresh();
374
+ },
375
+ onError: (error: AxiosError) => {
376
+ toast.open({
377
+ type: 'error',
378
+ title: t('global.notificationError'),
379
+ message: error.message,
380
+ });
381
+ },
382
+ });
383
+
384
+ const endPendingChat = useMutation({
385
+ mutationFn: (event: string) =>
386
+ apiDev.post('chats/end', {
387
+ chatId: chat.id ?? '',
388
+ event: event,
389
+ authorTimestamp: new Date().toISOString(),
390
+ authorFirstName: userInfo!.firstName,
391
+ authorId: userInfo!.idCode,
392
+ authorRole: userInfo!.authorities,
393
+ }),
394
+ onSuccess: async () => {
395
+ onRefresh();
396
+ },
397
+ onError: (error: AxiosError) => {
398
+ toast.open({
399
+ type: 'error',
400
+ title: t('global.notificationError'),
401
+ message: error.message,
402
+ });
403
+ },
404
+ });
405
+
406
+ const messageReadStatusRef = useRef({
407
+ messageId: null,
408
+ readTime: null,
409
+ });
410
+
411
+ const endUserFullName = getUserName();
412
+
413
+ useEffect(() => {
414
+ if (!messagesList) return;
415
+ let groupedMessages: GroupedMessage[] = [];
416
+ messagesList.forEach((message) => {
417
+ const lastGroup = groupedMessages[groupedMessages.length - 1];
418
+ if (
419
+ lastGroup &&
420
+ lastGroup.type === AUTHOR_ROLES.BACKOFFICE_USER &&
421
+ lastGroup.messages.at(-1) &&
422
+ message.event === CHAT_EVENTS.READ
423
+ ) {
424
+ lastGroup.messages.push(message);
425
+ return;
426
+ }
427
+ if (lastGroup?.type === message.authorRole) {
428
+ if (
429
+ !message.event ||
430
+ message.event === '' ||
431
+ message.event === 'greeting'
432
+ ) {
433
+ lastGroup.messages.push({ ...message });
434
+ } else if (
435
+ message.event === CHAT_EVENTS.WAITING_VALIDATION &&
436
+ chat.status === CHAT_STATUS.VALIDATING
437
+ ) {
438
+ groupedMessages.push({
439
+ name: 'Bürokratt',
440
+ type: 'buerokratt',
441
+ title: '',
442
+ messages: [{ ...message }],
443
+ });
444
+ } else {
445
+ groupedMessages.push({
446
+ name: '',
447
+ type: 'event',
448
+ title: '',
449
+ messages: [{ ...message }],
450
+ });
451
+ }
452
+ } else if (
453
+ !message.event ||
454
+ message.event === '' ||
455
+ message.event === 'greeting'
456
+ ) {
457
+ const isBackOfficeUser =
458
+ message.authorRole === 'backoffice-user'
459
+ ? `${message.authorFirstName} ${message.authorLastName}`
460
+ : BACKOFFICE_NAME.DEFAULT;
461
+ groupedMessages.push({
462
+ name:
463
+ message.authorRole === 'end-user'
464
+ ? endUserFullName
465
+ : isBackOfficeUser,
466
+ type: message.authorRole,
467
+ title: message.csaTitle ?? '',
468
+ messages: [{ ...message }],
469
+ });
470
+ } else if (
471
+ message.event === CHAT_EVENTS.WAITING_VALIDATION &&
472
+ chat.status === CHAT_STATUS.VALIDATING
473
+ ) {
474
+ groupedMessages.push({
475
+ name: 'Bürokratt',
476
+ type: 'buerokratt',
477
+ title: '',
478
+ messages: [{ ...message }],
479
+ });
480
+ } else {
481
+ groupedMessages.push({
482
+ name: '',
483
+ type: 'event',
484
+ title: '',
485
+ messages: [{ ...message }],
486
+ });
487
+ }
488
+ });
489
+ setMessageGroupsState(groupedMessages);
490
+ }, [messagesList, endUserFullName]);
491
+
492
+ useEffect(() => {
493
+ if (!chatRef.current || !messageGroups) return;
494
+ chatRef.current.scrollIntoView({ block: 'end', inline: 'end' });
495
+ }, [messageGroups, previewTypingMessage]);
496
+
497
+ const handleResponseTextSend = async (editMessage: boolean) => {
498
+ const newMessage: Message = {
499
+ chatId: chat.id,
500
+ authorRole: AUTHOR_ROLES.BACKOFFICE_USER,
501
+ content: responseText,
502
+ csaTitle: userInfo?.csaTitle ?? '',
503
+ authorTimestamp: new Date().toISOString(),
504
+ authorFirstName: userInfo?.displayName ?? '',
505
+ authorLastName: '',
506
+ authorId: userInfo?.idCode ?? '',
507
+ forwardedByUser: chat.forwardedByUser ?? '',
508
+ forwardedFromCsa: chat.forwardedFromCsa ?? '',
509
+ forwardedToCsa: chat.forwardedToCsa ?? '',
510
+ ...(editMessage && {
511
+ originalBaseId: selectedMessage?.id,
512
+ originalCreated: selectedMessage?.created,
513
+ }),
514
+ };
515
+
516
+ if (responseText !== '') {
517
+ const res = await postMessageMutation.mutateAsync({
518
+ message: newMessage,
519
+ editing: editMessage,
520
+ });
521
+ const message = { ...res.data.response, id: res.data.response.baseId };
522
+ if (selectedMessage) {
523
+ const index = messagesList.findIndex(
524
+ (m) => m.id === selectedMessage.id
525
+ );
526
+ const updatedMessages = [...messagesList];
527
+ updatedMessages[index] = message;
528
+ setMessagesList(updatedMessages);
529
+ } else {
530
+ setMessagesList((oldMessages) => [...oldMessages, message]);
531
+ }
532
+ setResponseText('');
533
+ setSelectedMessage(null);
534
+ }
535
+ };
536
+
537
+ const handleChatEvent = (event: string) => {
538
+ const newMessage: Message = {
539
+ chatId: chat.id,
540
+ authorRole: AUTHOR_ROLES.BACKOFFICE_USER,
541
+ content: '',
542
+ csaTitle: userInfo?.csaTitle ?? '',
543
+ event: event,
544
+ created: new Date().toLocaleString(),
545
+ authorTimestamp: new Date().toISOString(),
546
+ authorFirstName: userInfo?.displayName ?? '',
547
+ authorLastName: '',
548
+ authorId: userInfo?.idCode ?? '',
549
+ forwardedByUser: chat.forwardedByUser ?? '',
550
+ forwardedFromCsa: chat.forwardedFromCsa ?? '',
551
+ forwardedToCsa: chat.forwardedToCsa ?? '',
552
+ };
553
+
554
+ postEventMutation.mutate(newMessage);
555
+ setMessagesList((oldMessages) => [...oldMessages, newMessage]);
556
+ };
557
+
558
+ const handleSelectMessage = (message: Message) => {
559
+ if (selectedMessage?.id === message.id) {
560
+ setSelectedMessage(null);
561
+ setResponseText('');
562
+ } else {
563
+ setSelectedMessage(message);
564
+ setResponseText(message.content);
565
+ }
566
+ };
567
+
568
+ useQuery<{ config: BotConfig }>({
569
+ queryKey: ['configs/bot-config', 'prod'],
570
+ onSuccess(data: any) {
571
+ setIsChatEditingAllowed(data.response.isEditChatVisible === 'true');
572
+ },
573
+ });
574
+
575
+ const disableAskForPermission =
576
+ chat.customerSupportId != userInfo?.idCode ||
577
+ (latestPermissionMessageSeconds <= askPermissionsTimeoutInSeconds &&
578
+ latestPermissionMessageSeconds != 0);
579
+
580
+ const takeOverCondition =
581
+ chat.customerSupportId === '' ||
582
+ (chat.customerSupportId !== userInfo?.idCode &&
583
+ userInfo?.authorities.includes('ROLE_ADMINISTRATOR'));
584
+
585
+ return (
586
+ <div className="active-chat">
587
+ <div className="active-chat__body">
588
+ <div className="active-chat__header">
589
+ <Track direction="vertical" gap={8} align="left">
590
+ <p style={{ fontSize: 14, lineHeight: '1.5', color: '#4D4F5D' }}>
591
+ {t('chat.active.startedAt', {
592
+ date: format(new Date(chat.created), 'dd. MMMM Y HH:mm:ss', {
593
+ locale: et,
594
+ }),
595
+ })}
596
+ </p>
597
+ <h3>{endUserFullName}</h3>
598
+ </Track>
599
+ </div>
600
+
601
+ <div className="active-chat__group-wrapper">
602
+ {messageGroups?.map((group, index) => (
603
+ <div
604
+ className={clsx([
605
+ 'active-chat__group',
606
+ `active-chat__group--${group.type}`,
607
+ ])}
608
+ key={`${group.type}-${index}`}
609
+ >
610
+ {group.type === 'event' ? (
611
+ <ChatEvent message={group.messages[0]} />
612
+ ) : (
613
+ <>
614
+ <div className="active-chat__group-initials">
615
+ {group.type === 'buerokratt' || group.type === 'chatbot' ? (
616
+ <BykLogoWhite height={24} />
617
+ ) : (
618
+ <>
619
+ {group.name
620
+ .split(' ')
621
+ .map((n) => n[0])
622
+ .join('')
623
+ .toUpperCase()}
624
+ </>
625
+ )}
626
+ </div>
627
+ <div className="active-chat__group-name">
628
+ {group.name}
629
+ {group.title.length > 0 && (
630
+ <div className="title">{group.title}</div>
631
+ )}
632
+ </div>
633
+
634
+ <div className="active-chat__messages">
635
+ {group.messages.map((message, i) => (
636
+ <ChatMessage
637
+ message={message}
638
+ readStatus={messageReadStatusRef}
639
+ key={`${message.id ?? ''}-${i}`}
640
+ onSelect={(m) => {
641
+ if (
642
+ isChatEditingAllowed &&
643
+ chat.customerSupportId === userInfo.idCode &&
644
+ message.authorId === userInfo.idCode
645
+ )
646
+ handleSelectMessage(m);
647
+ }}
648
+ selected={selectedMessage?.id === message.id}
649
+ editableMessage={
650
+ isChatEditingAllowed &&
651
+ chat.customerSupportId === userInfo.idCode &&
652
+ message.authorId === userInfo.idCode
653
+ }
654
+ />
655
+ ))}
656
+ </div>
657
+ </>
658
+ )}
659
+ </div>
660
+ ))}
661
+ {/* Preview commented Out as requested by clients in task -1024- */}
662
+ {previewTypingMessage && (
663
+ <div className={clsx(['active-chat__group'])} key={`group`}>
664
+ <div className="active-chat__group-initials">
665
+ {<BykLogoWhite height={24} />}
666
+ </div>
667
+ <div className="active-chat__group-name">
668
+ {t('chat.userTyping')}
669
+ </div>
670
+ <div className="active-chat__messages">
671
+ <PreviewMessage
672
+ key={`preview-message`}
673
+ preview={previewTypingMessage ?? ''}
674
+ />
675
+ </div>
676
+ </div>
677
+ )}
678
+
679
+ <div id="anchor" ref={chatRef}></div>
680
+ </div>
681
+
682
+ {chat.customerSupportId == userInfo?.idCode &&
683
+ chat.status != CHAT_STATUS.IDLE && (
684
+ <>
685
+ {selectedMessage ? (
686
+ <div className="active-chat__toolbar edit-toolbar">
687
+ <div className="edit-toolbar__header">
688
+ Vestluse muutmine
689
+ <MdOutlineCreate className="active-chat__edit-icon" />
690
+ </div>
691
+ <div className="edit-toolbar__textarea">
692
+ <ChatTextArea
693
+ name="message"
694
+ label={t('')}
695
+ id="chatArea"
696
+ placeholder={t('chat.reply') + '...'}
697
+ minRows={1}
698
+ maxRows={8}
699
+ value={responseText}
700
+ onSubmit={(e) => handleResponseTextSend(true)}
701
+ maxLength={CHAT_INPUT_LENGTH}
702
+ onChange={(e) => setResponseText(e.target.value)}
703
+ />
704
+ </div>
705
+
706
+ <div className="edit-toolbar__edit-actions">
707
+ <Button
708
+ id="myButton"
709
+ appearance="primary"
710
+ size="s"
711
+ onClick={() => handleResponseTextSend(true)}
712
+ >
713
+ <Icon
714
+ icon={<MdDoneOutline fontSize={18} />}
715
+ size="medium"
716
+ />
717
+ </Button>
718
+ <Button
719
+ appearance="secondary"
720
+ size="s"
721
+ onClick={() => {
722
+ setSelectedMessage(null);
723
+ setResponseText('');
724
+ }}
725
+ >
726
+ <Icon icon={<MdClose fontSize={18} />} size="medium" />
727
+ </Button>
728
+ </div>
729
+ </div>
730
+ ) : (
731
+ <div className="active-chat__toolbar">
732
+ <Track>
733
+ <ChatTextArea
734
+ name="message"
735
+ label={t('')}
736
+ id="chatArea"
737
+ placeholder={t('chat.reply') + '...'}
738
+ minRows={1}
739
+ maxRows={8}
740
+ value={responseText}
741
+ onSubmit={(e) => handleResponseTextSend(false)}
742
+ maxLength={CHAT_INPUT_LENGTH}
743
+ onChange={(e) => setResponseText(e.target.value)}
744
+ />
745
+ <div className="active-chat__toolbar-actions">
746
+ <Button
747
+ id="myButton"
748
+ appearance="primary"
749
+ onClick={() => handleResponseTextSend(false)}
750
+ >
751
+ <Icon
752
+ icon={<MdOutlineSend fontSize={18} />}
753
+ size="medium"
754
+ />
755
+ <input
756
+ type="file"
757
+ ref={hiddenFileInputRef}
758
+ onChange={handleFileChange}
759
+ style={{ display: 'none' }}
760
+ />
761
+ </Button>
762
+ {isHiddenFeaturesEnabled && (
763
+ <Button
764
+ appearance="secondary"
765
+ onClick={handleUploadClick}
766
+ >
767
+ <Icon
768
+ icon={<MdOutlineAttachFile fontSize={18} />}
769
+ size="medium"
770
+ />
771
+ </Button>
772
+ )}
773
+ </div>
774
+ </Track>
775
+ </div>
776
+ )}
777
+ </>
778
+ )}
779
+
780
+ {takeOverCondition &&
781
+ chatCsaActive === true &&
782
+ chat.status != CHAT_STATUS.IDLE &&
783
+ chat.status != CHAT_STATUS.VALIDATING && (
784
+ <div className="active-chat__toolbar">
785
+ <Track justify="center">
786
+ <div className="active-chat__toolbar-actions">
787
+ <Button
788
+ appearance="primary"
789
+ style={{
790
+ backgroundColor: '#25599E',
791
+ color: '#FFFFFF',
792
+ borderRadius: '50px',
793
+ paddingLeft: '40px',
794
+ paddingRight: '40px',
795
+ }}
796
+ onClick={() => takeOverChatMutation.mutate()}
797
+ >
798
+ {t('chat.active.takeOver')}
799
+ </Button>
800
+ </div>
801
+ </Track>
802
+ </div>
803
+ )}
804
+
805
+ {chat.status === CHAT_STATUS.IDLE &&
806
+ (chat.customerSupportId === 'chatbot' ||
807
+ chat.customerSupportId != userInfo?.idCode) && (
808
+ <div className="active-chat__toolbar">
809
+ <Track justify="center">
810
+ <div className="active-chat__toolbar-actions">
811
+ <Button
812
+ appearance="primary"
813
+ style={{
814
+ backgroundColor: '#25599E',
815
+ color: '#FFFFFF',
816
+ borderRadius: '50px',
817
+ paddingLeft: '40px',
818
+ paddingRight: '40px',
819
+ }}
820
+ onClick={() => assignPendingChatMutation.mutate()}
821
+ >
822
+ {t('chat.active.takeOver')}
823
+ </Button>
824
+ </div>
825
+ </Track>
826
+ </div>
827
+ )}
828
+
829
+ {chat.status === CHAT_STATUS.IDLE &&
830
+ chat.customerSupportId != 'chatbot' &&
831
+ chat.customerSupportId === userInfo?.idCode && (
832
+ <div className="active-chat__toolbar">
833
+ <Track justify="center">
834
+ <div className="active-chat__toolbar-actions">
835
+ <Button
836
+ appearance="error"
837
+ style={{
838
+ borderRadius: '50px',
839
+ paddingLeft: '40px',
840
+ paddingRight: '40px',
841
+ }}
842
+ onClick={() => endPendingChat.mutate('user-not-reached')}
843
+ >
844
+ {t('chat.active.couldNotReachUser')}
845
+ </Button>
846
+ <Button
847
+ appearance="success"
848
+ style={{
849
+ borderRadius: '50px',
850
+ paddingLeft: '40px',
851
+ paddingRight: '40px',
852
+ }}
853
+ onClick={() => endPendingChat.mutate('user-reached')}
854
+ >
855
+ {t('chat.active.ContactedUser')}
856
+ </Button>
857
+ </div>
858
+ </Track>
859
+ </div>
860
+ )}
861
+ </div>
862
+ <div className="active-chat__side">
863
+ {(chat.customerSupportId === '' ||
864
+ chat.customerSupportId === userInfo?.idCode) &&
865
+ chat.status != CHAT_STATUS.IDLE &&
866
+ chat.status != CHAT_STATUS.VALIDATING && (
867
+ <div className="active-chat__side-actions">
868
+ <Button appearance="success" onClick={chatEnd()}>
869
+ {t('chat.active.endChat')}
870
+ </Button>
871
+ <Button
872
+ appearance="secondary"
873
+ disabled={
874
+ chat.customerSupportId != userInfo?.idCode ||
875
+ chat.endUserId != ''
876
+ }
877
+ onClick={() =>
878
+ handleChatEvent(CHAT_EVENTS.REQUESTED_AUTHENTICATION)
879
+ }
880
+ >
881
+ {t('chat.active.askAuthentication')}
882
+ </Button>
883
+ <Button
884
+ appearance="secondary"
885
+ disabled={chat.customerSupportId != userInfo?.idCode}
886
+ onClick={() => handleChatEvent(CHAT_EVENTS.CONTACT_INFORMATION)}
887
+ >
888
+ {t('chat.active.askForContact')}
889
+ </Button>
890
+ <div style={{ position: 'relative', display: 'inline-block' }}>
891
+ <Button
892
+ appearance="secondary"
893
+ style={{ width: '100%' }}
894
+ disabledWithoutStyle={disableAskForPermission}
895
+ disabled={chat.customerSupportId != userInfo?.idCode}
896
+ onClick={() => {
897
+ const message: Message | undefined = messagesList.findLast(
898
+ (e) => e.event === CHAT_EVENTS.ASK_PERMISSION
899
+ );
900
+ if (message != undefined) {
901
+ postMessageWithNewEventMutation.mutate(message);
902
+ } else {
903
+ handleChatEvent(CHAT_EVENTS.ASK_PERMISSION);
904
+ }
905
+ }}
906
+ >
907
+ {t('chat.active.askPermission')}
908
+ </Button>
909
+ {latestPermissionMessageSeconds <=
910
+ askPermissionsTimeoutInSeconds && (
911
+ <LoaderOverlay
912
+ maxPercent={askPermissionsTimeoutInSeconds}
913
+ currentPercent={latestPermissionMessageSeconds}
914
+ />
915
+ )}
916
+ </div>
917
+ <Button
918
+ appearance="secondary"
919
+ disabled={!chatCsaActive}
920
+ onClick={forwardToColleague()}
921
+ >
922
+ {t('chat.active.forwardToColleague')}
923
+ </Button>
924
+ {isHiddenFeaturesEnabled && (
925
+ <Button
926
+ appearance="secondary"
927
+ disabled={!chatCsaActive}
928
+ onClick={forwardToEstablishment()}
929
+ >
930
+ {t('chat.active.forwardToOrganization')}
931
+ </Button>
932
+ )}
933
+ {isHiddenFeaturesEnabled && (
934
+ <Button
935
+ appearance="secondary"
936
+ disabled={chat.customerSupportId != userInfo?.idCode}
937
+ onClick={sendToEmail()}
938
+ >
939
+ {t('chat.active.sendToEmail')}
940
+ </Button>
941
+ )}
942
+ {isHiddenFeaturesEnabled && (
943
+ <Button
944
+ appearance="secondary"
945
+ disabled={chat.customerSupportId != userInfo?.idCode}
946
+ onClick={StartAService()}
947
+ >
948
+ {t('chat.active.startService')}
949
+ </Button>
950
+ )}
951
+ </div>
952
+ )}
953
+ {chat.customerSupportId !== '' &&
954
+ chat.customerSupportId !== userInfo?.idCode &&
955
+ !chatCsaActive && (
956
+ <div className="active-chat__side-actions">
957
+ <Track gap={8} style={{ marginBottom: 36 }}>
958
+ <Label type="warning">!</Label>
959
+ <p className="csa-away">Nõustaja on eemal.</p>
960
+ </Track>
961
+ {userInfo?.authorities.some((authority) =>
962
+ [
963
+ ROLES.ROLE_ADMINISTRATOR,
964
+ ROLES.ROLE_CUSTOMER_SUPPORT_AGENT,
965
+ ].includes(authority as ROLES)
966
+ ) && (
967
+ <Button appearance="secondary" onClick={forwardToColleague()}>
968
+ {t('chat.active.forwardToColleague')}
969
+ </Button>
970
+ )}
971
+ </div>
972
+ )}
973
+ <div className="active-chat__side-meta">
974
+ <div>
975
+ <p>
976
+ <strong>ID</strong>
977
+ </p>
978
+ <p>{chat.id}</p>
979
+ </div>
980
+ <div>
981
+ <p>
982
+ <strong>{t('chat.endUser')}</strong>
983
+ </p>
984
+ <p>{endUserFullName}</p>
985
+ </div>
986
+ {chat.endUserId && (
987
+ <div>
988
+ <p>
989
+ <strong>{t('chat.endUserId')}</strong>
990
+ </p>
991
+ <p>{chat.endUserId ?? ''}</p>
992
+ </div>
993
+ )}
994
+ {chat.endUserEmail && (
995
+ <div>
996
+ <p>
997
+ <strong>{t('chat.endUserEmail')}</strong>
998
+ </p>
999
+ <p>{chat.endUserEmail}</p>
1000
+ </div>
1001
+ )}
1002
+ {chat.endUserPhone && (
1003
+ <div>
1004
+ <p>
1005
+ <strong>{t('chat.endUserPhoneNumber')}</strong>
1006
+ </p>
1007
+ <p>{chat.endUserPhone}</p>
1008
+ </div>
1009
+ )}
1010
+ {chat.customerSupportDisplayName && (
1011
+ <div>
1012
+ <p>
1013
+ <strong>{t('chat.csaName')}</strong>
1014
+ </p>
1015
+ <p>{chat.customerSupportDisplayName}</p>
1016
+ </div>
1017
+ )}
1018
+ <div>
1019
+ <p>
1020
+ <strong>{t('chat.startedAt')}</strong>
1021
+ </p>
1022
+ <p>
1023
+ {format(new Date(chat.created), 'dd. MMMM Y HH:mm:ss', {
1024
+ locale: et,
1025
+ }).toLowerCase()}
1026
+ </p>
1027
+ </div>
1028
+ <div>
1029
+ <p>
1030
+ <strong>{t('chat.device')}</strong>
1031
+ </p>
1032
+ <p>{chat.endUserOs}</p>
1033
+ </div>
1034
+ <div>
1035
+ <p>
1036
+ <strong>{t('chat.location')}</strong>
1037
+ </p>
1038
+ <p>{chat.endUserUrl}</p>
1039
+ </div>
1040
+ </div>
1041
+ </div>
1042
+ </div>
1043
+ );
1044
+
1045
+ function chatEnd() {
1046
+ return onChatEnd ? () => onChatEnd(chat) : undefined;
1047
+ }
1048
+
1049
+ function forwardToEstablishment() {
1050
+ return onForwardToEstablishment
1051
+ ? () => onForwardToEstablishment(chat)
1052
+ : undefined;
1053
+ }
1054
+
1055
+ function sendToEmail() {
1056
+ return onSendToEmail ? () => onSendToEmail(chat) : undefined;
1057
+ }
1058
+
1059
+ function forwardToColleague() {
1060
+ return onForwardToColleauge
1061
+ ? () => {
1062
+ onForwardToColleauge(chat);
1063
+ }
1064
+ : undefined;
1065
+ }
1066
+
1067
+ function StartAService() {
1068
+ return onStartAService ? () => onStartAService(chat) : undefined;
1069
+ }
1070
+
1071
+ function getUserName() {
1072
+ return chat.endUserFirstName !== '' && chat.endUserLastName !== ''
1073
+ ? `${chat.endUserFirstName} ${chat.endUserLastName}`
1074
+ : t('global.anonymous');
1075
+ }
1076
+
1077
+ async function handleFileRead(file: File): Promise<string | null> {
1078
+ if (!Object.values(AttachmentTypes).some((v) => v === file.type)) {
1079
+ return null;
1080
+ }
1081
+
1082
+ if (file.size > MESSAGE_FILE_SIZE_LIMIT) {
1083
+ return null;
1084
+ } else {
1085
+ return await convertBase64(file);
1086
+ }
1087
+ }
1088
+
1089
+ async function convertBase64(file: File): Promise<any> {
1090
+ return await new Promise((resolve, reject) => {
1091
+ const fileReader = new FileReader();
1092
+ fileReader.readAsDataURL(file);
1093
+ fileReader.onload = () => {
1094
+ resolve(fileReader.result);
1095
+ };
1096
+ fileReader.onerror = (error) => {
1097
+ reject(new Error('Error reading file'));
1098
+ };
1099
+ });
1100
+ }
1101
+ };
1102
+
1103
+ function filterMessages(messagesList: Message[], newMessage: Message) {
1104
+ return !messagesList.some(
1105
+ (existingMessage) =>
1106
+ existingMessage.id === newMessage.id &&
1107
+ existingMessage.event === newMessage.event
1108
+ );
1109
+ }
1110
+
1111
+ export default Chat;