@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,998 @@
1
+ import React, {FC, PropsWithChildren, useEffect, useMemo, useState} from 'react';
2
+ import {useTranslation} from 'react-i18next';
3
+ import {useMutation} from '@tanstack/react-query';
4
+ import {ColumnPinningState, createColumnHelper, PaginationState, SortingState,} from '@tanstack/react-table';
5
+ import {format} from 'date-fns';
6
+ import {AxiosError} from 'axios';
7
+ import './History.scss';
8
+ import {MdOutlineRemoveRedEye} from 'react-icons/md';
9
+
10
+ import {
11
+ Button,
12
+ Card,
13
+ DataTable,
14
+ Dialog,
15
+ Drawer,
16
+ FormDatepicker,
17
+ FormInput,
18
+ FormMultiselect,
19
+ HistoricalChat,
20
+ Icon,
21
+ Tooltip,
22
+ Track,
23
+ } from '../../../ui-components';
24
+
25
+ import {Chat as ChatType, CHAT_EVENTS, CHAT_STATUS} from '../../../types/chat';
26
+ import {apiDev} from '../../../services';
27
+ import {Controller, useForm} from 'react-hook-form';
28
+ import {useLocation, useSearchParams} from 'react-router-dom';
29
+ import {unifyDateFromat} from './unfiyDate';
30
+ import {et} from 'date-fns/locale';
31
+ import {useDebouncedCallback} from 'use-debounce';
32
+ import {UserInfo} from "../../../types/userInfo";
33
+ import {ToastContextType} from "../../../context";
34
+
35
+ type HistoryProps = {
36
+ user: UserInfo | null;
37
+ toastContext: ToastContextType | null;
38
+ onMessageClick?: (message: any) => void;
39
+ showComment?: boolean;
40
+ showStatus?: boolean;
41
+ }
42
+
43
+ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({user, toastContext, onMessageClick, showComment = true, showStatus = true}) => {
44
+ const {t, i18n} = useTranslation();
45
+ const toast = toastContext;
46
+ const userInfo = user;
47
+ const routerLocation = useLocation();
48
+ const params = new URLSearchParams(routerLocation.search);
49
+ let passedChatId = new URLSearchParams(routerLocation.search).get('chat');
50
+ const passedStartDate = params.get('start');
51
+ const passedEndDate = params.get('end');
52
+ const passedCustomerSupportIds = params.getAll('customerSupportIds');
53
+ const [search, setSearch] = useState('');
54
+ const [selectedChat, setSelectedChat] = useState<ChatType | null>(null);
55
+ const [searchParams, setSearchParams] = useSearchParams();
56
+ const [statusChangeModal, setStatusChangeModal] = useState<string | null>(
57
+ null
58
+ );
59
+ const [chatState, setChatState] = useState<string | null>(statusChangeModal);
60
+ const [pagination, setPagination] = useState<PaginationState>({
61
+ pageIndex: searchParams.get('page')
62
+ ? parseInt(searchParams.get('page') as string) - 1
63
+ : 0,
64
+ pageSize: 10,
65
+ });
66
+ const [sorting, setSorting] = useState<SortingState>([]);
67
+ const columnPinning: ColumnPinningState = {
68
+ left: [],
69
+ right: ['detail'],
70
+ };
71
+ const [totalPages, setTotalPages] = useState<number>(1);
72
+ const [initialLoad, setInitialLoad] = useState<boolean>(true);
73
+ const [filteredEndedChatsList, setFilteredEndedChatsList] = useState<
74
+ ChatType[]
75
+ >([]);
76
+
77
+ const [messagesTrigger, setMessagesTrigger] = useState(false);
78
+ const [selectedColumns, setSelectedColumns] = useState<string[]>([]);
79
+ const [customerSupportAgents, setCustomerSupportAgents] = useState<any[]>([]);
80
+ const [counterKey, setCounterKey] = useState<number>(0)
81
+
82
+ const {control, watch} = useForm<{
83
+ startDate: Date | string;
84
+ endDate: Date | string;
85
+ }>({
86
+ defaultValues: {
87
+ startDate: passedStartDate
88
+ ? unifyDateFromat(passedStartDate)
89
+ : new Date(
90
+ new Date().getUTCFullYear(),
91
+ new Date().getUTCMonth(),
92
+ new Date().getUTCDate()
93
+ ),
94
+ endDate: passedEndDate
95
+ ? unifyDateFromat(passedEndDate)
96
+ : new Date(
97
+ new Date().getUTCFullYear(),
98
+ new Date().getUTCMonth(),
99
+ new Date().getUTCDate()
100
+ ),
101
+ },
102
+ });
103
+
104
+ const startDate = watch('startDate');
105
+ const endDate = watch('endDate');
106
+
107
+ const debouncedGetAllEnded = useDebouncedCallback((search) => {
108
+ getAllEndedChats.mutate({
109
+ startDate: format(new Date(startDate), 'yyyy-MM-dd'),
110
+ endDate: format(new Date(endDate), 'yyyy-MM-dd'),
111
+ customerSupportIds: passedCustomerSupportIds,
112
+ pagination,
113
+ sorting,
114
+ search,
115
+ });
116
+ }, 500);
117
+
118
+ useEffect(() => {
119
+ if (passedChatId != null) {
120
+ getChatById.mutate();
121
+ passedChatId = null;
122
+ }
123
+ }, [passedChatId]);
124
+
125
+ const fetchData = async () => {
126
+ setInitialLoad(false);
127
+ try {
128
+ const response = await apiDev.get('/accounts/get-page-preference', {
129
+ params: {
130
+ user_id: userInfo?.idCode,
131
+ page_name: window.location.pathname,
132
+ },
133
+ });
134
+ if (response.data.pageResults !== undefined) {
135
+ const newSelectedColumns = response.data?.selectedColumns.length === 1 && response.data?.selectedColumns[0] === "" ? [] : response.data?.selectedColumns;
136
+ setSelectedColumns(newSelectedColumns)
137
+ const updatedPagination = updatePagePreference(
138
+ response.data.pageResults ?? 10
139
+ );
140
+ setCounterKey(counterKey + 1)
141
+ getAllEndedChats.mutate({
142
+ startDate: format(new Date(startDate), 'yyyy-MM-dd'),
143
+ endDate: format(new Date(endDate), 'yyyy-MM-dd'),
144
+ customerSupportIds: passedCustomerSupportIds,
145
+ pagination: updatedPagination,
146
+ sorting,
147
+ search,
148
+ });
149
+ }
150
+ } catch (err) {
151
+ console.error('Failed to fetch data');
152
+ }
153
+ };
154
+
155
+ const updatePagePreference = (pageResults: number): PaginationState => {
156
+ const updatedPagination: PaginationState = {
157
+ ...pagination,
158
+ pageSize: pageResults,
159
+ };
160
+ setPagination(updatedPagination);
161
+ return updatedPagination;
162
+ };
163
+
164
+ useEffect(() => {
165
+ if (initialLoad) {
166
+ fetchData();
167
+ } else {
168
+ getAllEndedChats.mutate({
169
+ startDate: format(new Date(startDate), 'yyyy-MM-dd'),
170
+ endDate: format(new Date(endDate), 'yyyy-MM-dd'),
171
+ customerSupportIds: passedCustomerSupportIds,
172
+ pagination,
173
+ sorting,
174
+ search,
175
+ });
176
+ }
177
+ }, [selectedColumns]);
178
+
179
+ useEffect(() => {
180
+ listCustomerSupportAgents.mutate();
181
+ }, []);
182
+
183
+ const updatePagePreferences = useMutation({
184
+ mutationFn: (data: {
185
+ page_results: number;
186
+ selected_columns: string[];
187
+ }) => {
188
+ return apiDev.post('accounts/update-page-preference', {
189
+ user_id: userInfo?.idCode,
190
+ page_name: window.location.pathname,
191
+ page_results: data.page_results,
192
+ selected_columns: `{"${data.selected_columns.join('","')}"}`
193
+ });
194
+ },
195
+ });
196
+
197
+ const getAllEndedChats = useMutation({
198
+ mutationFn: (data: {
199
+ startDate: string;
200
+ endDate: string;
201
+ customerSupportIds: string[];
202
+ pagination: PaginationState;
203
+ sorting: SortingState;
204
+ search: string;
205
+ }) => {
206
+ let sortBy = 'created desc';
207
+ if (sorting.length > 0) {
208
+ const sortType = sorting[0].desc ? 'desc' : 'asc';
209
+ sortBy = `${sorting[0].id} ${sortType}`;
210
+ }
211
+
212
+ return apiDev.post('agents/chats/ended', {
213
+ customerSupportIds: data.customerSupportIds,
214
+ startDate: data.startDate,
215
+ endDate: data.endDate,
216
+ page: data.pagination.pageIndex + 1,
217
+ page_size: data.pagination.pageSize,
218
+ sorting: sortBy,
219
+ search,
220
+ });
221
+ },
222
+ onSuccess: (res: any) => {
223
+ if (selectedChat)
224
+ setSelectedChat({
225
+ ...selectedChat,
226
+ lastMessageEvent: res.data.response[0].lastMessageEvent,
227
+ lastMessageTimestamp: res.data.response[0].lastMessageTimestamp,
228
+ userDisplayName: res.data.response[0].userDisplayName,
229
+ });
230
+ filterChatsList(res.data.response ?? []);
231
+ setTotalPages(res?.data?.response[0]?.totalPages ?? 1);
232
+ },
233
+ });
234
+
235
+ const getChatById = useMutation({
236
+ mutationFn: () =>
237
+ apiDev.post('chats/get', {
238
+ chatId: passedChatId,
239
+ }),
240
+ onSuccess: (res: any) => {
241
+ setSelectedChat(res.data.response);
242
+ setChatState(res.data.response);
243
+ },
244
+ });
245
+
246
+ const listCustomerSupportAgents = useMutation({
247
+ mutationFn: () =>
248
+ apiDev.post('accounts/customer-support-agents', {
249
+ page: 0,
250
+ page_size: 99999,
251
+ sorting: 'name asc',
252
+ show_active_only: false,
253
+ roles: ['ROLE_CUSTOMER_SUPPORT_AGENT'],
254
+ }),
255
+ onSuccess: (res: any) => {
256
+ setCustomerSupportAgents([
257
+ {label: '-', value: ','},
258
+ {label: 'Bürokratt', value: 'chatbot'},
259
+ ...res.data.response.map((item) => ({
260
+ label: [item.firstName, item.lastName].join(' ').trim(),
261
+ value: item.idCode,
262
+ })),
263
+ ]);
264
+ },
265
+ });
266
+
267
+ const visibleColumnOptions = useMemo(
268
+ () => [
269
+ {label: t('chat.history.startTime'), value: 'created'},
270
+ {label: t('chat.history.endTime'), value: 'ended'},
271
+ {label: t('chat.history.csaName'), value: 'customerSupportFullName'},
272
+ {label: t('global.name'), value: 'endUserName'},
273
+ {label: t('global.idCode'), value: 'endUserId'},
274
+ {label: t('chat.history.contact'), value: 'contactsMessage'},
275
+ {label: t('chat.history.comment'), value: 'comment'},
276
+ {label: t('chat.history.rating'), value: 'feedbackRating'},
277
+ {label: t('chat.history.feedback'), value: 'feedbackText'},
278
+ {label: t('global.status'), value: 'status'},
279
+ {label: 'ID', value: 'id'},
280
+ {label: 'www', value: 'www'},
281
+ ],
282
+ [t]
283
+ );
284
+
285
+ const chatStatusChangeMutation = useMutation({
286
+ mutationFn: async (data: { chatId: string | number; event: string }) => {
287
+ const changeableTo = [
288
+ CHAT_EVENTS.CLIENT_LEFT_WITH_ACCEPTED.toUpperCase(),
289
+ CHAT_EVENTS.CLIENT_LEFT_WITH_NO_RESOLUTION.toUpperCase(),
290
+ CHAT_EVENTS.ACCEPTED.toUpperCase(),
291
+ CHAT_EVENTS.ANSWERED.toUpperCase(),
292
+ CHAT_EVENTS.CLIENT_LEFT_FOR_UNKNOWN_REASONS.toUpperCase(),
293
+ CHAT_EVENTS.CLIENT_LEFT.toUpperCase(),
294
+ CHAT_EVENTS.HATE_SPEECH.toUpperCase(),
295
+ CHAT_EVENTS.OTHER.toUpperCase(),
296
+ CHAT_EVENTS.TERMINATED.toUpperCase(),
297
+ CHAT_EVENTS.RESPONSE_SENT_TO_CLIENT_EMAIL.toUpperCase(),
298
+ ];
299
+ const isChangeable = changeableTo.includes(data.event);
300
+
301
+ if (selectedChat?.lastMessageEvent === data.event.toLowerCase()) return;
302
+
303
+ if (!isChangeable) return;
304
+
305
+ await apiDev.post('chats/status', {
306
+ chatId: selectedChat!.id,
307
+ event: data.event.toUpperCase(),
308
+ authorTimestamp: new Date().toISOString(),
309
+ authorFirstName: userInfo!.firstName,
310
+ authorId: userInfo!.idCode,
311
+ authorRole: userInfo!.authorities,
312
+ });
313
+ },
314
+ onSuccess: () => {
315
+ setMessagesTrigger(!messagesTrigger);
316
+ getAllEndedChats.mutate({
317
+ startDate: format(new Date(startDate), 'yyyy-MM-dd'),
318
+ endDate: format(new Date(endDate), 'yyyy-MM-dd'),
319
+ customerSupportIds: passedCustomerSupportIds,
320
+ pagination,
321
+ sorting,
322
+ search,
323
+ });
324
+ toast?.open({
325
+ type: 'success',
326
+ title: t('global.notification'),
327
+ message: t('toast.success.chatStatusChanged'),
328
+ });
329
+ setStatusChangeModal(null);
330
+ },
331
+ onError: (error: AxiosError) => {
332
+ toast?.open({
333
+ type: 'error',
334
+ title: t('global.notificationError'),
335
+ message: error.message,
336
+ });
337
+ },
338
+ onSettled: () => setStatusChangeModal(null),
339
+ });
340
+
341
+ const chatCommentChangeMutation = useMutation({
342
+ mutationFn: (data: {
343
+ chatId: string | number;
344
+ comment: string;
345
+ authorDisplayName: string;
346
+ }) => apiDev.post('comments/history', data),
347
+ onSuccess: (res, {chatId, comment}) => {
348
+ const updatedChatList = filteredEndedChatsList.map((chat) =>
349
+ chat.id === chatId ? {...chat, comment} : chat
350
+ );
351
+ filterChatsList(updatedChatList);
352
+ if (selectedChat)
353
+ setSelectedChat({
354
+ ...selectedChat,
355
+ comment,
356
+ commentAddedDate: res.data.response[0].created,
357
+ commentAuthor: res.data.response[0].authorDisplayName,
358
+ });
359
+ toast?.open({
360
+ type: 'success',
361
+ title: t('global.notification'),
362
+ message: t('toast.success.chatCommentChanged'),
363
+ });
364
+ },
365
+ onError: (error: AxiosError) => {
366
+ toast?.open({
367
+ type: 'error',
368
+ title: t('global.notificationError'),
369
+ message: error.message,
370
+ });
371
+ },
372
+ });
373
+
374
+ const columnHelper = createColumnHelper<ChatType>();
375
+
376
+ const copyValueToClipboard = async (value: string) => {
377
+ await navigator.clipboard.writeText(value);
378
+
379
+ toast?.open({
380
+ type: 'success',
381
+ title: t('global.notification'),
382
+ message: t('toast.success.copied'),
383
+ });
384
+ };
385
+
386
+ const commentView = (props: any) =>
387
+ props.getValue() && (
388
+ <Tooltip content={props.getValue()}>
389
+ <span
390
+ onClick={() => copyValueToClipboard(props.getValue())}
391
+ style={{cursor: 'pointer'}}
392
+ >
393
+ {props.getValue().length <= 25
394
+ ? props.getValue()
395
+ : `${props.getValue()?.slice(0, 25)}...`}
396
+ </span>
397
+ </Tooltip>
398
+ );
399
+
400
+ const feedbackTextView = (props: any) => {
401
+ const value = props.getValue() ?? '';
402
+
403
+ return (
404
+ <Tooltip content={value}>
405
+ <span style={{minWidth: '250px'}}>
406
+ {value.length < 30 ? value : `${value?.slice?.(0, 30)}...`}
407
+ </span>
408
+ </Tooltip>
409
+ );
410
+ };
411
+
412
+ const wwwView = (props: any) => (
413
+ <Tooltip content={props.getValue() ?? ''}>
414
+ <button
415
+ onClick={() => copyValueToClipboard(props.getValue())}
416
+ style={{cursor: 'pointer'}}
417
+ >
418
+ {props.getValue() ?? ''}
419
+ </button>
420
+ </Tooltip>
421
+ );
422
+
423
+ const statusView = (props: any) => {
424
+ const isLastMessageEvent =
425
+ props.row.original.lastMessageEvent != null &&
426
+ props.row.original.lastMessageEvent !== 'message-read'
427
+ ? t('chat.plainEvents.' + props.row.original.lastMessageEvent)
428
+ : t('chat.status.ended');
429
+ return props.getValue() === CHAT_STATUS.ENDED ? isLastMessageEvent : '';
430
+ };
431
+
432
+ const idView = (props: any) => (
433
+ <Tooltip content={props.getValue()}>
434
+ <span
435
+ onClick={() => copyValueToClipboard(props.getValue())}
436
+ style={{cursor: 'pointer'}}
437
+ >
438
+ {props.getValue().split('-')[0]}
439
+ </span>
440
+ </Tooltip>
441
+ );
442
+
443
+ const detailsView = (props: any) => (
444
+ <Button
445
+ appearance="text"
446
+ onClick={() => {
447
+ setSelectedChat(props.row.original);
448
+ setChatState(props.row.original.lastMessageEvent);
449
+ }}
450
+ >
451
+ <Icon icon={<MdOutlineRemoveRedEye color={'rgba(0,0,0,0.54)'}/>}/>
452
+ {t('global.view')}
453
+ </Button>
454
+ );
455
+
456
+ const endedChatsColumns = useMemo(
457
+ () => [
458
+ columnHelper.accessor('created', {
459
+ id: 'created',
460
+ header: t('chat.history.startTime') ?? '',
461
+ cell: (props) =>
462
+ format(
463
+ new Date(props.getValue()),
464
+ 'dd.MM.yyyy HH:mm:ss',
465
+ i18n.language === 'et' ? {locale: et} : undefined
466
+ ),
467
+ }),
468
+ columnHelper.accessor('ended', {
469
+ id: 'ended',
470
+ header: t('chat.history.endTime') ?? '',
471
+ cell: (props) =>
472
+ format(
473
+ new Date(props.getValue()),
474
+ 'dd.MM.yyyy HH:mm:ss',
475
+ i18n.language === 'et' ? {locale: et} : undefined
476
+ ),
477
+ }),
478
+ columnHelper.accessor('customerSupportDisplayName', {
479
+ id: 'customerSupportDisplayName',
480
+ header: i18n.t('chat.history.csaName') || '',
481
+ cell: (info) => {
482
+ const row = info.row.original;
483
+ const lastMessage = row.lastMessage;
484
+ const displayName = info.getValue();
485
+ if(displayName) {
486
+ return displayName;
487
+ } else {
488
+ return lastMessage.startsWith('Suunan') ? 'Bürokratt' : '\u00A0-';
489
+ }
490
+ },
491
+ }),
492
+ columnHelper.accessor(
493
+ (row) => `${row.endUserFirstName} ${row.endUserLastName}`,
494
+ {
495
+ id: `endUserName`,
496
+ header: t('global.name') ?? '',
497
+ }
498
+ ),
499
+ columnHelper.accessor('endUserId', {
500
+ id: 'endUserId',
501
+ header: t('global.idCode') ?? '',
502
+ }),
503
+ columnHelper.accessor('contactsMessage', {
504
+ id: 'contactsMessage',
505
+ header: t('chat.history.contact') ?? '',
506
+ cell: (props) => (props.getValue() ? t('global.yes') : t('global.no')),
507
+ }),
508
+ columnHelper.accessor('comment', {
509
+ id: 'comment',
510
+ header: t('chat.history.comment') ?? '',
511
+ cell: commentView,
512
+ }),
513
+ columnHelper.accessor('rating', {
514
+ id: 'feedbackRating',
515
+ header: t('chat.history.rating') ?? '',
516
+ cell: (props) =>
517
+ props.getValue() && <span>{`${props.getValue()}/10`}</span>,
518
+ }),
519
+ columnHelper.accessor('feedback', {
520
+ id: 'feedbackText',
521
+ header: t('chat.history.feedback') ?? '',
522
+ cell: feedbackTextView,
523
+ }),
524
+ columnHelper.accessor('status', {
525
+ id: 'status',
526
+ header: t('global.status') ?? '',
527
+ cell: statusView,
528
+ sortingFn: (a, b, isAsc) => {
529
+ const statusA =
530
+ a.getValue('status') === CHAT_STATUS.ENDED
531
+ ? t('chat.plainEvents.' + (a.original.lastMessageEvent ?? ''))
532
+ : '';
533
+ const statusB =
534
+ b.getValue('status') === CHAT_STATUS.ENDED
535
+ ? t('chat.plainEvents.' + (b.original.lastMessageEvent ?? ''))
536
+ : '';
537
+ return (
538
+ statusA.localeCompare(statusB, undefined, {
539
+ numeric: true,
540
+ sensitivity: 'base',
541
+ }) * (isAsc ? 1 : -1)
542
+ );
543
+ },
544
+ }),
545
+ columnHelper.accessor('id', {
546
+ id: 'id',
547
+ header: 'ID',
548
+ cell: idView,
549
+ }),
550
+ columnHelper.accessor('endUserUrl', {
551
+ id: 'www',
552
+ header: 'www',
553
+ cell: wwwView,
554
+ }),
555
+ columnHelper.display({
556
+ id: 'detail',
557
+ cell: detailsView,
558
+ meta: {
559
+ size: '3%',
560
+ sticky: 'right',
561
+ },
562
+ }),
563
+ ],
564
+ []
565
+ );
566
+
567
+ const handleChatStatusChange = (event: string) => {
568
+ if (!selectedChat) return;
569
+ chatStatusChangeMutation.mutate({
570
+ chatId: selectedChat.id,
571
+ event: event.toUpperCase(),
572
+ });
573
+ };
574
+
575
+ const handleCommentChange = (comment: string) => {
576
+ if (!selectedChat) return;
577
+ chatCommentChangeMutation.mutate({
578
+ chatId: selectedChat.id,
579
+ comment,
580
+ authorDisplayName: userInfo!.displayName,
581
+ });
582
+ };
583
+
584
+ const getFilteredColumns = () => {
585
+ if (selectedColumns.length === 0) return endedChatsColumns;
586
+ return endedChatsColumns.filter((c) =>
587
+ ['detail', 'forward', ...selectedColumns].includes(c.id ?? '')
588
+ );
589
+ };
590
+
591
+ const filterChatsList = (chatsList: ChatType[]) => {
592
+ const startDate = Date.parse(
593
+ format(new Date(control._formValues.startDate), 'MM/dd/yyyy')
594
+ );
595
+
596
+ const endDate = Date.parse(
597
+ format(new Date(control._formValues.endDate), 'MM/dd/yyyy')
598
+ );
599
+
600
+ setFilteredEndedChatsList(
601
+ chatsList.filter((c) => {
602
+ const ended = Date.parse(format(new Date(c.ended), 'MM/dd/yyyy'));
603
+ return ended >= startDate && ended <= endDate;
604
+ })
605
+ );
606
+ };
607
+
608
+ const endUserFullName = getUserName();
609
+
610
+ if (!filteredEndedChatsList) return <>Loading... {{filteredEndedChatsList}} something is wrong </>;
611
+
612
+ return (
613
+ <>
614
+ <h1>{t('chat.history.title')}</h1>
615
+
616
+ <Card>
617
+ <Track gap={16}>
618
+ <FormInput
619
+ className="input-wrapper"
620
+ label={t('chat.history.searchChats')}
621
+ hideLabel
622
+ name="searchChats"
623
+ placeholder={t('chat.history.searchChats') + '...'}
624
+ onChange={(e) => {
625
+ setSearch(e.target.value);
626
+ debouncedGetAllEnded(e.target.value);
627
+ }}
628
+ />
629
+ <Track style={{width: '100%'}} gap={16}>
630
+ <Track gap={10}>
631
+ <p>{t('global.from')}</p>
632
+ <Controller
633
+ name="startDate"
634
+ control={control}
635
+ render={({field}) => {
636
+ return (
637
+ <FormDatepicker
638
+ {...field}
639
+ label={''}
640
+ value={field.value ?? new Date()}
641
+ onChange={(v) => {
642
+ field.onChange(v);
643
+ const start = format(new Date(v), 'yyyy-MM-dd');
644
+ setSearchParams((params) => {
645
+ params.set('start', start);
646
+ return params;
647
+ });
648
+ getAllEndedChats.mutate({
649
+ startDate: start,
650
+ endDate: format(new Date(endDate), 'yyyy-MM-dd'),
651
+ customerSupportIds: passedCustomerSupportIds,
652
+ pagination,
653
+ sorting,
654
+ search,
655
+ });
656
+ }}
657
+ />
658
+ );
659
+ }}
660
+ />
661
+ </Track>
662
+ <Track gap={10}>
663
+ <p>{t('global.to')}</p>
664
+ <Controller
665
+ name="endDate"
666
+ control={control}
667
+ render={({field}) => {
668
+ return (
669
+ <FormDatepicker
670
+ {...field}
671
+ label={''}
672
+ value={field.value ?? new Date()}
673
+ onChange={(v) => {
674
+ field.onChange(v);
675
+ const end = format(new Date(v), 'yyyy-MM-dd');
676
+ setSearchParams((params) => {
677
+ params.set('end', end);
678
+ return params;
679
+ });
680
+ getAllEndedChats.mutate({
681
+ startDate: format(new Date(startDate), 'yyyy-MM-dd'),
682
+ endDate: end,
683
+ customerSupportIds: passedCustomerSupportIds,
684
+ pagination,
685
+ sorting,
686
+ search,
687
+ });
688
+ }}
689
+ />
690
+ );
691
+ }}
692
+ />
693
+ </Track>
694
+ <FormMultiselect
695
+ key={counterKey}
696
+ name="visibleColumns"
697
+ label={t('')}
698
+ placeholder={t('chat.history.chosenColumn')}
699
+ options={visibleColumnOptions}
700
+ selectedOptions={visibleColumnOptions.filter((o) =>
701
+ selectedColumns.includes(o.value)
702
+ )}
703
+ onSelectionChange={(selection) => {
704
+ const columns = selection?.map((s) => s.value) ?? [];
705
+ setSelectedColumns(columns);
706
+ updatePagePreferences.mutate({
707
+ page_results: pagination.pageSize,
708
+ selected_columns: columns
709
+ })
710
+ }}
711
+ />
712
+ <FormMultiselect
713
+ name="agent"
714
+ label={t('')}
715
+ placeholder={t('chat.history.chosenCsa')}
716
+ options={customerSupportAgents}
717
+ selectedOptions={customerSupportAgents.filter((item) =>
718
+ passedCustomerSupportIds.includes(item.value)
719
+ )}
720
+ onSelectionChange={(selection) => {
721
+ setSearchParams((params) => {
722
+ params.delete('customerSupportIds');
723
+ params.set('page', '1');
724
+ selection?.forEach((s) =>
725
+ params.append('customerSupportIds', s.value)
726
+ );
727
+ return params;
728
+ });
729
+
730
+ setPagination({pageIndex: 0, pageSize: pagination.pageSize});
731
+
732
+ getAllEndedChats.mutate({
733
+ startDate,
734
+ endDate,
735
+ customerSupportIds: selection?.map((s) => s.value) || [],
736
+ pagination: {pageIndex: 0, pageSize: pagination.pageSize},
737
+ sorting,
738
+ search,
739
+ });
740
+ }}
741
+ />
742
+ </Track>
743
+ </Track>
744
+ </Card>
745
+
746
+ <div className="card-drawer-container">
747
+ <div className="card-wrapper">
748
+ <Card>
749
+ <DataTable
750
+ data={filteredEndedChatsList}
751
+ sortable
752
+ columns={getFilteredColumns()}
753
+ selectedRow={(row) => row.original.id === selectedChat?.id}
754
+ pagination={pagination}
755
+ columnPinning={columnPinning}
756
+ sorting={sorting}
757
+ setPagination={(state: PaginationState) => {
758
+ if (
759
+ state.pageIndex === pagination.pageIndex &&
760
+ state.pageSize === pagination.pageSize
761
+ )
762
+ return;
763
+ setPagination(state);
764
+ updatePagePreferences.mutate({
765
+ page_results: state.pageSize,
766
+ selected_columns: selectedColumns
767
+ });
768
+ getAllEndedChats.mutate({
769
+ startDate: format(new Date(startDate), 'yyyy-MM-dd'),
770
+ endDate: format(new Date(endDate), 'yyyy-MM-dd'),
771
+ customerSupportIds: passedCustomerSupportIds,
772
+ pagination: state,
773
+ sorting,
774
+ search,
775
+ });
776
+ }}
777
+ setSorting={(state: SortingState) => {
778
+ setSorting(state);
779
+ getAllEndedChats.mutate({
780
+ startDate: format(new Date(startDate), 'yyyy-MM-dd'),
781
+ endDate: format(new Date(endDate), 'yyyy-MM-dd'),
782
+ customerSupportIds: passedCustomerSupportIds,
783
+ pagination,
784
+ sorting: state,
785
+ search,
786
+ });
787
+ }}
788
+ isClientSide={false}
789
+ pagesCount={totalPages}
790
+ />
791
+ </Card>
792
+ </div>
793
+
794
+ {selectedChat && (
795
+ <>
796
+ <div className="drawer-container">
797
+ <Drawer
798
+ title={
799
+ selectedChat.endUserFirstName !== '' &&
800
+ selectedChat.endUserLastName !== ''
801
+ ? `${selectedChat.endUserFirstName} ${selectedChat.endUserLastName}`
802
+ : t('global.anonymous')
803
+ }
804
+ onClose={() => setSelectedChat(null)}
805
+ >
806
+ <HistoricalChat
807
+ chat={selectedChat}
808
+ trigger={messagesTrigger}
809
+ onChatStatusChange={setStatusChangeModal}
810
+ selectedStatus={chatState}
811
+ onCommentChange={handleCommentChange}
812
+ toastContext={toastContext}
813
+ showComment={showComment}
814
+ showStatus={showStatus}
815
+ onMessageClick={(message) => {
816
+ onMessageClick?.(message);
817
+ }}
818
+ />
819
+ </Drawer>
820
+ </div>
821
+ <div className="side-meta">
822
+ <div>
823
+ <p>
824
+ <strong>ID</strong>
825
+ </p>
826
+ <p>{selectedChat.id}</p>
827
+ </div>
828
+ <div>
829
+ <p>
830
+ <strong>{t('chat.endUser')}</strong>
831
+ </p>
832
+ <p>{endUserFullName}</p>
833
+ </div>
834
+ {selectedChat.endUserId && (
835
+ <div>
836
+ <p>
837
+ <strong>{t('chat.endUserId')}</strong>
838
+ </p>
839
+ <p>{selectedChat.endUserId ?? ''}</p>
840
+ </div>
841
+ )}
842
+ {selectedChat.endUserEmail && (
843
+ <div>
844
+ <p>
845
+ <strong>{t('chat.endUserEmail')}</strong>
846
+ </p>
847
+ <p>{selectedChat.endUserEmail}</p>
848
+ </div>
849
+ )}
850
+ {selectedChat.endUserPhone && (
851
+ <div>
852
+ <p>
853
+ <strong>{t('chat.endUserPhoneNumber')}</strong>
854
+ </p>
855
+ <p>{selectedChat.endUserPhone}</p>
856
+ </div>
857
+ )}
858
+ {selectedChat.customerSupportDisplayName && (
859
+ <div>
860
+ <p>
861
+ <strong>{t('chat.csaName')}</strong>
862
+ </p>
863
+ <p>{selectedChat.customerSupportDisplayName}</p>
864
+ </div>
865
+ )}
866
+ <div>
867
+ <p>
868
+ <strong>{t('chat.startedAt')}</strong>
869
+ </p>
870
+ <p>
871
+ {format(
872
+ new Date(selectedChat.created),
873
+ 'dd. MMMM Y HH:mm:ss',
874
+ {
875
+ locale: et,
876
+ }
877
+ ).toLowerCase()}
878
+ </p>
879
+ </div>
880
+ <div>
881
+ <p>
882
+ <strong>{t('chat.device')}</strong>
883
+ </p>
884
+ <p>{selectedChat.endUserOs}</p>
885
+ </div>
886
+ <div>
887
+ <p>
888
+ <strong>{t('chat.location')}</strong>
889
+ </p>
890
+ <p>{selectedChat.endUserUrl}</p>
891
+ </div>
892
+ {selectedChat.comment && (
893
+ <div>
894
+ <p>
895
+ <strong>{t('chat.history.comment')}</strong>
896
+ </p>
897
+ <p>{selectedChat.comment}</p>
898
+ </div>
899
+ )}
900
+ {selectedChat.commentAuthor && (
901
+ <div>
902
+ <p>
903
+ <strong>{t('chat.history.commentAuthor')}</strong>
904
+ </p>
905
+ <p>{selectedChat.commentAuthor}</p>
906
+ </div>
907
+ )}
908
+ {selectedChat.commentAddedDate && (
909
+ <div>
910
+ <p>
911
+ <strong>{t('chat.history.commentAddedDate')}</strong>
912
+ </p>
913
+ <p>
914
+ {format(
915
+ new Date(selectedChat.commentAddedDate),
916
+ 'dd.MM.yyyy'
917
+ )}
918
+ </p>
919
+ </div>
920
+ )}
921
+ {selectedChat.lastMessageEvent && (
922
+ <div>
923
+ <p>
924
+ <strong>{t('global.status')}</strong>
925
+ </p>
926
+ <p>
927
+ {t('chat.plainEvents.' + selectedChat.lastMessageEvent)}
928
+ </p>
929
+ </div>
930
+ )}
931
+ {selectedChat.userDisplayName && (
932
+ <div>
933
+ <p>
934
+ <strong>{t('chat.history.statusAdder')}</strong>
935
+ </p>
936
+ <p>{selectedChat.userDisplayName}</p>
937
+ </div>
938
+ )}
939
+ {selectedChat.lastMessageTimestamp && (
940
+ <div>
941
+ <p>
942
+ <strong>{t('chat.history.statusAddedDate')}</strong>
943
+ </p>
944
+ <p>
945
+ {format(
946
+ new Date(selectedChat.lastMessageTimestamp),
947
+ 'dd.MM.yyyy'
948
+ )}
949
+ </p>
950
+ </div>
951
+ )}
952
+ </div>
953
+ </>
954
+ )}
955
+ </div>
956
+
957
+ {statusChangeModal && (
958
+ <Dialog
959
+ title={t('chat.changeStatus')}
960
+ onClose={() => setStatusChangeModal(null)}
961
+ footer={
962
+ <>
963
+ <Button
964
+ appearance="secondary"
965
+ onClick={() => {
966
+ setChatState(null);
967
+ setStatusChangeModal(null);
968
+ }}
969
+ >
970
+ {t('global.cancel')}
971
+ </Button>
972
+ <Button
973
+ appearance="error"
974
+ onClick={() => {
975
+ setChatState(statusChangeModal);
976
+ handleChatStatusChange(statusChangeModal);
977
+ }}
978
+ >
979
+ {t('global.yes')}
980
+ </Button>
981
+ </>
982
+ }
983
+ >
984
+ <p>{t('global.removeValidation')}</p>
985
+ </Dialog>
986
+ )}
987
+ </>
988
+ );
989
+
990
+ function getUserName() {
991
+ return selectedChat?.endUserFirstName !== '' &&
992
+ selectedChat?.endUserLastName !== ''
993
+ ? `${selectedChat?.endUserFirstName} ${selectedChat?.endUserLastName}`
994
+ : t('global.anonymous');
995
+ }
996
+ };
997
+
998
+ export default ChatHistory;