@buerokratt-ria/common-gui-components 0.0.57 → 0.0.59

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 (26) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/package.json +1 -1
  3. package/templates/history-page/src/History.scss +33 -3
  4. package/templates/history-page/src/components/ChatMetadataPanel/ChatMetadataPanel.scss +18 -0
  5. package/templates/history-page/src/components/ChatMetadataPanel/index.tsx +206 -0
  6. package/templates/history-page/src/components/ChatMetadataPanelItem/index.tsx +17 -0
  7. package/templates/history-page/src/components/FilterTag/FilterTag.scss +42 -0
  8. package/templates/history-page/src/components/FilterTag/index.tsx +16 -0
  9. package/templates/history-page/src/components/HeaderCombobox/index.tsx +66 -0
  10. package/templates/history-page/src/components/QualitySettings/QualitySettings.scss +19 -0
  11. package/templates/history-page/src/components/QualitySettings/index.tsx +115 -0
  12. package/templates/history-page/src/components/SelectedFilterTags/SelectedFilterTags.scss +36 -0
  13. package/templates/history-page/src/components/SelectedFilterTags/index.tsx +224 -0
  14. package/templates/history-page/src/components/index.tsx +6 -0
  15. package/templates/history-page/src/index.tsx +943 -209
  16. package/templates/history-page/src/types/index.ts +17 -0
  17. package/translations/en/common.json +22 -2
  18. package/translations/et/common.json +22 -2
  19. package/types/chat.ts +3 -0
  20. package/ui-components/FormElements/FormCombobox/FormCombobox.scss +252 -0
  21. package/ui-components/FormElements/FormCombobox/index.tsx +334 -0
  22. package/ui-components/FormElements/index.tsx +1 -0
  23. package/ui-components/HistoricalChat/ChatMessage.tsx +24 -9
  24. package/ui-components/HistoricalChat/index.tsx +16 -6
  25. package/ui-components/Icon/index.tsx +1 -0
  26. package/ui-components/index.tsx +2 -0
@@ -1,17 +1,17 @@
1
1
  import {FC, PropsWithChildren, useEffect, useMemo, useRef, useState} from 'react';
2
+ import type {ComponentProps} from 'react';
2
3
  import {useTranslation} from 'react-i18next';
3
- import {useMutation} from '@tanstack/react-query';
4
+ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
4
5
  import {ColumnPinningState, createColumnHelper, PaginationState, SortingState,} from '@tanstack/react-table';
5
6
  import {endOfDay, format, formatISO, startOfDay} from "date-fns";
6
7
  import {AxiosError} from 'axios';
7
8
  import './History.scss';
8
- import {MdOutlineRemoveRedEye } from 'react-icons/md';
9
+ import {MdOutlineRemoveRedEye} from 'react-icons/md';
9
10
  import {CgSpinner} from 'react-icons/cg';
10
11
 
11
12
  import {
12
13
  Button,
13
14
  Card,
14
- ClearFiltersButton,
15
15
  DataTable,
16
16
  Dialog,
17
17
  Drawer,
@@ -38,6 +38,8 @@ import {ToastContextType} from "../../../context";
38
38
  import {getDomainsArray} from "../../../utils/multiDomain-utils";
39
39
  import {StoreState} from "../../../store";
40
40
  import {saveFile} from "../../../services/file";
41
+ import {ChatMetadataPanel, HeaderCombobox, QualitySettings, SelectedFilterTags} from './components';
42
+ import { CharMeasurementType } from './types';
41
43
 
42
44
  type HistoryProps = {
43
45
  user: UserInfo | null;
@@ -62,7 +64,132 @@ type ExportResult = {
62
64
  chatIds: string[];
63
65
  };
64
66
 
67
+ type QualitySettingsConfig = {
68
+ readonly chatAnalysisEnabled: boolean;
69
+ readonly chatAnalysisTheme: string[];
70
+ readonly chatAnalysisBykResponseQuality: string[];
71
+ readonly chatAnalysisFollowUpAction: string[];
72
+ };
73
+
74
+ export type DomainSelection = {
75
+ readonly id: string;
76
+ readonly name: string;
77
+ readonly url: string;
78
+ readonly selected: boolean;
79
+ };
80
+
81
+ type FeedbackConfig = {
82
+ readonly isFiveRatingScale?: string | boolean;
83
+ };
84
+
85
+ const GLOBAL_FEEDBACK_CONFIG_DOMAIN = 'none';
86
+
87
+ export const MEASUREMENT_TYPES = {
88
+ THEME: 'THEME',
89
+ QUALITY: 'QUALITY',
90
+ FOLLOW_UP_ACTION: 'FOLLOW_UP_ACTION',
91
+ } as const;
92
+
93
+ export type MeasurementType =
94
+ (typeof MEASUREMENT_TYPES)[keyof typeof MEASUREMENT_TYPES];
95
+
96
+
97
+ export const postQualityMeasurements = (body: {
98
+ readonly chatUuid: string;
99
+ readonly type: MeasurementType;
100
+ readonly value: string | string[];
101
+ }) =>
102
+ apiDev.post('chats/quality/measurements', body);
103
+
104
+ export const getQualityMeasurements = (params: {
105
+ readonly chatUuid: string;
106
+ }) =>
107
+ apiDev.get<{
108
+ readonly response: CharMeasurementType[];
109
+ }>('chats/quality/measurements', { params });
110
+
111
+ export const getWidgetData = async (userId: string) => {
112
+ const { data } = await apiDev.get<DomainSelection[]>('accounts/widget-data', {
113
+ params: {
114
+ user_id: userId,
115
+ },
116
+ });
117
+ return data;
118
+ }
119
+
120
+ const loadQualitySettingsConfig = async (domain: string): Promise<QualitySettingsConfig> => {
121
+ const response = await apiDev.get<{
122
+ readonly response: {
123
+ readonly chatAnalysisEnabled: boolean;
124
+ readonly chatAnalysisTheme: string; // can be an empty string if no themes are defined
125
+ readonly chatAnalysisBykResponseQuality: string; // can be an empty string if no qualities are defined
126
+ readonly chatAnalysisFollowUpAction: string; // can be an empty string if no follow-up actions are defined
127
+ };
128
+ }>('/configs/chat-analysis', {
129
+ params: { domain },
130
+ });
131
+ return {
132
+ chatAnalysisEnabled: response.data.response.chatAnalysisEnabled,
133
+ chatAnalysisTheme: response.data.response.chatAnalysisTheme.split(',').filter(Boolean),
134
+ chatAnalysisBykResponseQuality: response.data.response.chatAnalysisBykResponseQuality.split(',').filter(Boolean),
135
+ chatAnalysisFollowUpAction: response.data.response.chatAnalysisFollowUpAction.split(',').filter(Boolean),
136
+ };
137
+ }
138
+
139
+ const loadFeedbackConfig = async (): Promise<FeedbackConfig> => {
140
+ const response = await apiDev.get('/configs/feedback', {
141
+ params: { domain: GLOBAL_FEEDBACK_CONFIG_DOMAIN },
142
+ });
143
+
144
+ return response.data.response ?? response.data;
145
+ }
146
+
147
+ const isFiveRatingScaleEnabled = (value?: string | boolean | null) =>
148
+ value === true || value === 'true';
149
+
150
+ const formatChatAnalysisCell = (
151
+ value: string[] | null | undefined,
152
+ selectionEmptiedLabel: string
153
+ ) => {
154
+ if (!Array.isArray(value) || value.length === 0) {
155
+ return '';
156
+ }
157
+
158
+ if (value.length === 1 && value[0] === '') {
159
+ return selectionEmptiedLabel;
160
+ }
161
+
162
+ return value.join(', ');
163
+ };
164
+
65
165
  const ALL_COLUMNS_VALUE = '__all__';
166
+ const BOOLEAN_SORT_COLUMN_IDS = new Set([
167
+ 'authenticatedPerson',
168
+ 'istest',
169
+ 'isPreserve',
170
+ ]);
171
+ const CHAT_STATUSES = [
172
+ CHAT_EVENTS.ACCEPTED,
173
+ CHAT_EVENTS.CLIENT_LEFT_FOR_UNKNOWN_REASONS,
174
+ CHAT_EVENTS.CLIENT_LEFT_WITH_ACCEPTED,
175
+ CHAT_EVENTS.CLIENT_LEFT_WITH_NO_RESOLUTION,
176
+ CHAT_EVENTS.HATE_SPEECH,
177
+ CHAT_EVENTS.OTHER,
178
+ CHAT_EVENTS.RESPONSE_SENT_TO_CLIENT_EMAIL,
179
+ ];
180
+
181
+ const getEndedChatsSortBy = (sorting: SortingState) => {
182
+ if (sorting.length === 0) {
183
+ return 'created desc';
184
+ }
185
+
186
+ const [sortingObject] = sorting;
187
+ const sortType = BOOLEAN_SORT_COLUMN_IDS.has(sortingObject.id)
188
+ ? sortingObject.desc ? 'asc' : 'desc'
189
+ : sortingObject.desc ? 'desc' : 'asc';
190
+
191
+ return `${sortingObject.id} ${sortType}`;
192
+ };
66
193
 
67
194
  const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
68
195
  user,
@@ -81,6 +208,7 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
81
208
  delegatedStartDate = null
82
209
  }) => {
83
210
  const {t, i18n} = useTranslation();
211
+ const queryClient = useQueryClient();
84
212
  const toast = toastContext;
85
213
  const userInfo = user;
86
214
  const routerLocation = useLocation();
@@ -89,6 +217,7 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
89
217
  const passedStartDate = delegatedStartDate ?? params.get("start");
90
218
  const passedEndDate = delegatedEndDate ?? params.get("end");
91
219
  const skipNextSelectedColumnsEffect = useRef(false);
220
+ const skipInitialTableHeaderFilterEffect = useRef(true);
92
221
  const [selectedChat, setSelectedChat] = useState<ChatType | null>(null);
93
222
  const [searchParams, setSearchParams] = useSearchParams();
94
223
  const [statusChangeModal, setStatusChangeModal] = useState<string | null>(
@@ -119,7 +248,7 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
119
248
 
120
249
  const useStore = userDomains;
121
250
  const [updateKey, setUpdateKey] = useState<number>(0)
122
- const currentDomains = useStore.getState().userDomains;
251
+ const currentDomains = useStore.getState().userDomains as string[];
123
252
  const multiDomainEnabled = import.meta.env.REACT_APP_ENABLE_MULTI_DOMAIN?.toLowerCase() === 'true';
124
253
  const testMessageEnabled = import.meta.env.REACT_APP_SHOW_TEST_MESSAGE?.toLowerCase() === 'true';
125
254
  const envVal = import.meta.env.REACT_APP_SHOW_TEST_CONVERSATIONS;
@@ -129,6 +258,12 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
129
258
  const loadingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
130
259
  const timeoutAbortRef = useRef(false);
131
260
 
261
+ const userWidgetDomains = useQuery<DomainSelection[]>({
262
+ queryKey: ['accounts/widget-data', userInfo?.idCode],
263
+ queryFn: () => getWidgetData(userInfo!.idCode),
264
+ enabled: !!userInfo?.idCode,
265
+ });
266
+
132
267
  const parseDateParam = (dateString: string | null) => {
133
268
  if (!dateString) return new Date();
134
269
  return new Date(dateString.split("+")[0]);
@@ -285,6 +420,19 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
285
420
  },
286
421
  });
287
422
 
423
+ const selectedChatDomainUuid = useMemo(() => {
424
+ return userWidgetDomains.data?.find(domain => domain.url === selectedChat?.endUserUrl)?.id ?? null;
425
+ }, [selectedChat]);
426
+ const qualitySettingsConfigQuery = useQuery<QualitySettingsConfig>({
427
+ queryKey: ['configs/chat-analysis'],
428
+ queryFn: () => loadQualitySettingsConfig(selectedChatDomainUuid ? selectedChatDomainUuid : GLOBAL_FEEDBACK_CONFIG_DOMAIN),
429
+ });
430
+
431
+ const isChatAnalysisEnabled = useMemo(() => {
432
+ return qualitySettingsConfigQuery.data?.chatAnalysisEnabled ?? false;
433
+ }, [qualitySettingsConfigQuery.data]);
434
+ console.log("IS CHAT ANALYSIS ENABLED", isChatAnalysisEnabled);
435
+
288
436
  const getAllEndedChats = useMutation({
289
437
  mutationFn: (data: {
290
438
  startDate: string;
@@ -292,27 +440,25 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
292
440
  pagination: PaginationState;
293
441
  sorting: SortingState;
294
442
  search: string;
443
+ urls?: string[];
295
444
  }) => {
296
445
  abortRef.current?.abort();
297
446
  abortRef.current = new AbortController();
298
447
 
299
- let sortBy = 'created desc';
300
- if (sorting.length > 0) {
301
- const sortType = sorting[0].desc ? 'desc' : 'asc';
302
- sortBy = `${sorting[0].id} ${sortType}`;
303
- }
448
+ const sortBy = getEndedChatsSortBy(data.sorting);
304
449
 
305
450
  return apiDevEnded.post('agents/chats/ended', {
306
451
  startDate: formatISO(startOfDay(new Date(data.startDate))),
307
452
  endDate: formatISO(endOfDay(new Date(data.endDate))),
308
- urls: getDomainsArray(currentDomains),
309
- showTest: showTest,
453
+ ...endedChatsFilterBody,
454
+ ...(data.urls?.length ? {urls: getDomainsArray(data.urls)} : {}),
310
455
  page: data.pagination.pageIndex + 1,
311
456
  page_size: data.pagination.pageSize,
312
457
  sorting: sortBy,
313
458
  search,
314
459
  },
315
460
  {
461
+ params: { isChatAnalysisEnabled },
316
462
  signal: abortRef.current.signal
317
463
  }
318
464
  );
@@ -390,8 +536,16 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
390
536
 
391
537
  columns.splice(5, 0, {label: t('global.preserve'), value: 'isPreserve'});
392
538
 
539
+ if (isChatAnalysisEnabled) {
540
+ columns.push(
541
+ {label: t('chat.history.theme'), value: 'theme'},
542
+ {label: t('chat.history.responseQuality'), value: 'responseQuality'},
543
+ {label: t('chat.history.followUpStatus'), value: 'followUpStatus'}
544
+ );
545
+ }
546
+
393
547
  return columns;
394
- }, [t, showEmail, testMessageEnabled])
548
+ }, [t, showEmail, testMessageEnabled, isChatAnalysisEnabled])
395
549
 
396
550
  const visibleColumnOptions = useMemo(() => [
397
551
  {label: t('chat.history.chooseAll'), value: ALL_COLUMNS_VALUE},
@@ -732,6 +886,335 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
732
886
  </Button>
733
887
  );
734
888
 
889
+ const customerSupportAgentsQuery = useQuery({
890
+ queryKey: ['customer-support-agents'],
891
+ queryFn: () =>
892
+ apiDev.post<{
893
+ readonly response: {
894
+ readonly login: string;
895
+ readonly firstName: string;
896
+ readonly lastName: string;
897
+ readonly idCode: string;
898
+ readonly displayName: string;
899
+ readonly csaTitle: string;
900
+ readonly csaEmail: string;
901
+ readonly department: string;
902
+ readonly jiraAccountId: string;
903
+ readonly smaxAccountId: string;
904
+ readonly authorities: string[];
905
+ readonly customerSupportStatus: string;
906
+ readonly statusComment: string;
907
+ readonly statusCommentTimeStamp: string;
908
+ readonly domains: (string | null)[] | null;
909
+ readonly totalPages: number;
910
+ }[];
911
+ }>('accounts/customer-support-agents', {
912
+ page: 0,
913
+ page_size: 99999,
914
+ sorting: 'name asc',
915
+ show_active_only: false,
916
+ roles: ['ROLE_CUSTOMER_SUPPORT_AGENT'],
917
+ }),
918
+ select: (result) => {
919
+ return [
920
+ { label: t('chat.history.chooseAll'), value: ALL_COLUMNS_VALUE },
921
+ { label: 'Bürokratt', value: 'chatbot' },
922
+ ...result.data.response.map((item) => ({
923
+ label: [item.firstName, item.lastName].join(' ').trim(),
924
+ value: item.idCode,
925
+ })),
926
+ ]
927
+ },
928
+ });
929
+
930
+ const tableHeaderForm = useForm<{
931
+ readonly csaIdCodesFilter: string[];
932
+ readonly feedbackRatings: string[];
933
+ readonly showAuthenticatedPerson?: boolean;
934
+ readonly isTestFilter?: boolean;
935
+ readonly isPreserveFilter?: boolean;
936
+ readonly domains: string[];
937
+ readonly status: string[];
938
+ readonly theme: string[];
939
+ readonly responseQuality: string[];
940
+ readonly followUpStatus: string[];
941
+ }>({
942
+ defaultValues: {
943
+ csaIdCodesFilter: [],
944
+ feedbackRatings: [],
945
+ showAuthenticatedPerson: undefined,
946
+ isTestFilter: undefined,
947
+ isPreserveFilter: undefined,
948
+ domains: [],
949
+ status: [],
950
+ theme: [],
951
+ responseQuality: [],
952
+ followUpStatus: [],
953
+ },
954
+ });
955
+ const { reset: resetTableHeaderForm, setValue: setTableHeaderValue } = tableHeaderForm;
956
+ const csaIdCodesFilter = tableHeaderForm.watch('csaIdCodesFilter');
957
+ const feedbackRatings = tableHeaderForm.watch('feedbackRatings');
958
+ const showAuthenticatedPerson = tableHeaderForm.watch('showAuthenticatedPerson');
959
+ const isTestFilter = tableHeaderForm.watch('isTestFilter');
960
+ const isPreserveFilter = tableHeaderForm.watch('isPreserveFilter');
961
+ const domains = tableHeaderForm.watch('domains');
962
+ const status = tableHeaderForm.watch('status');
963
+ const theme = tableHeaderForm.watch('theme');
964
+ const responseQuality = tableHeaderForm.watch('responseQuality');
965
+ const followUpStatus = tableHeaderForm.watch('followUpStatus');
966
+
967
+ useEffect(() => {
968
+ if (skipInitialTableHeaderFilterEffect.current) {
969
+ skipInitialTableHeaderFilterEffect.current = false;
970
+ return;
971
+ }
972
+
973
+ const resetPagination = { pageIndex: 0, pageSize: pagination.pageSize };
974
+ setPagination(resetPagination);
975
+ setSearchParams((params) => {
976
+ params.set("page", "1");
977
+ return params;
978
+ });
979
+ getAllEndedChats.mutate({
980
+ startDate: formatISO(startOfDay(new Date(startDate))),
981
+ endDate: formatISO(endOfDay(new Date(endDate))),
982
+ pagination: resetPagination,
983
+ sorting,
984
+ search,
985
+ });
986
+ }, [csaIdCodesFilter, feedbackRatings, showAuthenticatedPerson, isTestFilter, isPreserveFilter, domains, status, theme, responseQuality, followUpStatus]);
987
+
988
+ const getBooleanComboboxValue = (value?: boolean) =>
989
+ value === undefined ? '' : String(value);
990
+ const getBooleanFormValue = (value: string) =>
991
+ value ? value === 'true' : undefined;
992
+ const getBooleanApiFilterValue = (value?: boolean) =>
993
+ value === undefined ? [] : [value];
994
+ const getRealStringFilterValues = (values: string[]) =>
995
+ values.filter((value) => value !== ALL_COLUMNS_VALUE);
996
+ const getAllStringFilterValues = (options: { readonly value: string }[]) =>
997
+ getRealStringFilterValues(options.map((option) => option.value));
998
+ const normalizeAllOptionFilterValues = (
999
+ values: string[],
1000
+ currentValues: string[],
1001
+ allValues: string[]
1002
+ ) => {
1003
+ const currentAllSelected = currentValues.includes(ALL_COLUMNS_VALUE);
1004
+ const nextAllSelected = values.includes(ALL_COLUMNS_VALUE);
1005
+
1006
+ if (nextAllSelected && !currentAllSelected) {
1007
+ return [ALL_COLUMNS_VALUE, ...allValues];
1008
+ }
1009
+
1010
+ if (!nextAllSelected && currentAllSelected) {
1011
+ return [];
1012
+ }
1013
+
1014
+ const realValues = getRealStringFilterValues(values);
1015
+
1016
+ if (allValues.length > 0 && allValues.every((value) => realValues.includes(value))) {
1017
+ return [ALL_COLUMNS_VALUE, ...allValues];
1018
+ }
1019
+
1020
+ return realValues;
1021
+ };
1022
+ const getRealCsaFilterValues = (values: string[]) =>
1023
+ getRealStringFilterValues(values);
1024
+ const getAllCsaFilterValues = () =>
1025
+ getRealCsaFilterValues(customerSupportAgentsQuery.data?.map((option) => option.value) ?? []);
1026
+ const getFeedbackRatingFilterValues = (values: string[]) =>
1027
+ values
1028
+ .map((value) => Number(value))
1029
+ .filter((value) => Number.isInteger(value));
1030
+ const normalizeCsaFilterValues = (values: string[]) => {
1031
+ const currentAllSelected = csaIdCodesFilter.includes(ALL_COLUMNS_VALUE);
1032
+ const nextAllSelected = values.includes(ALL_COLUMNS_VALUE);
1033
+ const allCsaFilterValues = getAllCsaFilterValues();
1034
+
1035
+ if (nextAllSelected && !currentAllSelected) {
1036
+ return [ALL_COLUMNS_VALUE, ...allCsaFilterValues];
1037
+ }
1038
+
1039
+ if (!nextAllSelected && currentAllSelected) {
1040
+ return [];
1041
+ }
1042
+
1043
+ const realValues = getRealCsaFilterValues(values);
1044
+
1045
+ if (allCsaFilterValues.length > 0 && allCsaFilterValues.every((value) => realValues.includes(value))) {
1046
+ return [ALL_COLUMNS_VALUE, ...allCsaFilterValues];
1047
+ }
1048
+
1049
+ return realValues;
1050
+ };
1051
+ const csaFilterTagValues = getRealCsaFilterValues(csaIdCodesFilter);
1052
+ const csaFilterTagLabelsByValue = useMemo(() => {
1053
+ return new Map(
1054
+ (customerSupportAgentsQuery.data ?? []).map((option) => [option.value, option.label])
1055
+ );
1056
+ }, [customerSupportAgentsQuery.data]);
1057
+ const getCsaFilterTagLabel = (value: string) => csaFilterTagLabelsByValue.get(value) ?? value;
1058
+ const removeSelectedFilterTag: ComponentProps<typeof SelectedFilterTags>['onRemove'] = (filter, value) => {
1059
+ switch (filter) {
1060
+ case 'csaIdCodesFilter':
1061
+ setTableHeaderValue(
1062
+ filter,
1063
+ getRealCsaFilterValues(csaIdCodesFilter).filter((item) => item !== value)
1064
+ );
1065
+ return;
1066
+ case 'showAuthenticatedPerson':
1067
+ case 'isTestFilter':
1068
+ case 'isPreserveFilter':
1069
+ setTableHeaderValue(filter, undefined);
1070
+ return;
1071
+ case 'domains':
1072
+ setTableHeaderValue(filter, getRealStringFilterValues(domains).filter((item) => item !== value));
1073
+ return;
1074
+ case 'feedbackRatings':
1075
+ setTableHeaderValue(filter, getRealStringFilterValues(feedbackRatings).filter((item) => item !== value));
1076
+ return;
1077
+ case 'status':
1078
+ setTableHeaderValue(filter, getRealStringFilterValues(status).filter((item) => item !== value));
1079
+ return;
1080
+ case 'theme':
1081
+ setTableHeaderValue(filter, getRealStringFilterValues(theme).filter((item) => item !== value));
1082
+ return;
1083
+ case 'responseQuality':
1084
+ setTableHeaderValue(filter, getRealStringFilterValues(responseQuality).filter((item) => item !== value));
1085
+ return;
1086
+ case 'followUpStatus':
1087
+ setTableHeaderValue(filter, getRealStringFilterValues(followUpStatus).filter((item) => item !== value));
1088
+ }
1089
+ };
1090
+
1091
+ const globalFeedbackConfigQuery = useQuery<FeedbackConfig>({
1092
+ queryKey: ['configs/feedback', GLOBAL_FEEDBACK_CONFIG_DOMAIN],
1093
+ queryFn: loadFeedbackConfig,
1094
+ });
1095
+
1096
+ const tableHeaderQualitySettingsConfigQuery = useQuery<QualitySettingsConfig>({
1097
+ queryKey: ['configs/chat-analysis', GLOBAL_FEEDBACK_CONFIG_DOMAIN],
1098
+ queryFn: () => loadQualitySettingsConfig(GLOBAL_FEEDBACK_CONFIG_DOMAIN),
1099
+ });
1100
+
1101
+ const realRatingOptions = useMemo(() => {
1102
+ const isFiveRatingScale = isFiveRatingScaleEnabled(globalFeedbackConfigQuery.data?.isFiveRatingScale);
1103
+ const ratingMin = isFiveRatingScale ? 1 : 0;
1104
+ const ratingMax = isFiveRatingScale ? 5 : 10;
1105
+
1106
+ return Array.from({ length: ratingMax - ratingMin + 1 }, (_, index) => {
1107
+ const rating = ratingMin + index;
1108
+
1109
+ return {
1110
+ label: String(rating),
1111
+ value: String(rating),
1112
+ };
1113
+ });
1114
+ }, [globalFeedbackConfigQuery.data?.isFiveRatingScale]);
1115
+
1116
+ const ratingOptions = useMemo(() => [
1117
+ { label: t('chat.history.chooseAll'), value: ALL_COLUMNS_VALUE },
1118
+ ...realRatingOptions,
1119
+ ], [t, realRatingOptions]);
1120
+
1121
+ const realStatusOptions = useMemo(() => {
1122
+ return CHAT_STATUSES.map((chatStatus) => ({
1123
+ label: t(`chat.plainEvents.${chatStatus}`),
1124
+ value: chatStatus,
1125
+ }));
1126
+ }, [t]);
1127
+
1128
+ const statusOptions = useMemo(() => [
1129
+ { label: t('chat.history.chooseAll'), value: ALL_COLUMNS_VALUE },
1130
+ ...realStatusOptions,
1131
+ ], [t, realStatusOptions]);
1132
+
1133
+ const domainOptions = useMemo(() => [
1134
+ { label: t('chat.history.chooseAll'), value: ALL_COLUMNS_VALUE },
1135
+ ...currentDomains.map((domain) => ({
1136
+ label: domain,
1137
+ value: domain,
1138
+ })),
1139
+ ], [t, currentDomains]);
1140
+
1141
+ const realThemeOptions = tableHeaderQualitySettingsConfigQuery.data?.chatAnalysisTheme.map(item => {
1142
+ return { label: item, value: item }
1143
+ }) ?? [];
1144
+ const realResponseQualityOptions = tableHeaderQualitySettingsConfigQuery.data?.chatAnalysisBykResponseQuality.map(item => {
1145
+ return { label: item, value: item }
1146
+ }) ?? [];
1147
+ const realFollowUpStatusOptions = tableHeaderQualitySettingsConfigQuery.data?.chatAnalysisFollowUpAction.map(item => {
1148
+ return { label: item, value: item }
1149
+ }) ?? [];
1150
+
1151
+ const themeOptions = useMemo(() => [
1152
+ { label: t('chat.history.chooseAll'), value: ALL_COLUMNS_VALUE },
1153
+ ...realThemeOptions,
1154
+ ], [t, realThemeOptions]);
1155
+
1156
+ const responseQualityOptions = useMemo(() => [
1157
+ { label: t('chat.history.chooseAll'), value: ALL_COLUMNS_VALUE },
1158
+ ...realResponseQualityOptions,
1159
+ ], [t, realResponseQualityOptions]);
1160
+
1161
+ const followUpStatusOptions = useMemo(() => [
1162
+ { label: t('chat.history.chooseAll'), value: ALL_COLUMNS_VALUE },
1163
+ ...realFollowUpStatusOptions,
1164
+ ], [t, realFollowUpStatusOptions]);
1165
+
1166
+ const feedbackRatingFilterValues = getRealStringFilterValues(feedbackRatings);
1167
+ const statusFilterValues = getRealStringFilterValues(status);
1168
+ const domainFilterValues = getRealStringFilterValues(domains);
1169
+ const themeFilterValues = getRealStringFilterValues(theme);
1170
+ const responseQualityFilterValues = getRealStringFilterValues(responseQuality);
1171
+ const followUpStatusFilterValues = getRealStringFilterValues(followUpStatus);
1172
+
1173
+ const endedChatsFilterBody = useMemo(() => {
1174
+ const currentCustomerSupportIds = getRealCsaFilterValues(csaIdCodesFilter);
1175
+ const currentFeedbackRatings = getFeedbackRatingFilterValues(feedbackRatings);
1176
+ const currentStatusValues = getRealStringFilterValues(status);
1177
+ const currentDomainValues = getRealStringFilterValues(domains);
1178
+ const currentIsTestValues = getBooleanApiFilterValue(isTestFilter);
1179
+ const currentShowAuthenticatedPersonValues = getBooleanApiFilterValue(showAuthenticatedPerson);
1180
+ const currentIsPreserveValues = getBooleanApiFilterValue(isPreserveFilter);
1181
+ const currentThemeValues = getRealStringFilterValues(theme);
1182
+ const currentResponseQualityValues = getRealStringFilterValues(responseQuality);
1183
+ const currentFollowUpStatusValues = getRealStringFilterValues(followUpStatus);
1184
+
1185
+ return {
1186
+ urls: getDomainsArray(currentDomainValues.length > 0 ? currentDomainValues : currentDomains),
1187
+ showTest: showTest,
1188
+ theme: currentThemeValues,
1189
+ responseQuality: currentResponseQualityValues,
1190
+ followUpStatus: currentFollowUpStatusValues,
1191
+ ...(currentCustomerSupportIds.length > 0 && {customerSupportIds: currentCustomerSupportIds}),
1192
+ ...(currentFeedbackRatings.length > 0 && {feedbackRatings: currentFeedbackRatings}),
1193
+ ...(currentIsTestValues.length > 0 && {isTest: currentIsTestValues}),
1194
+ ...(currentShowAuthenticatedPersonValues.length > 0 && {authenticatedChats: currentShowAuthenticatedPersonValues}),
1195
+ ...(currentIsPreserveValues.length > 0 && {isPreserve: currentIsPreserveValues}),
1196
+ ...(currentStatusValues.length > 0 && {status: currentStatusValues}),
1197
+ };
1198
+ }, [
1199
+ csaIdCodesFilter,
1200
+ currentDomains,
1201
+ domains,
1202
+ feedbackRatings,
1203
+ followUpStatus,
1204
+ isPreserveFilter,
1205
+ isTestFilter,
1206
+ responseQuality,
1207
+ showAuthenticatedPerson,
1208
+ showTest,
1209
+ status,
1210
+ theme,
1211
+ ]);
1212
+
1213
+ const booleanFilterOptions = useMemo(() => [
1214
+ { label: t('global.yes') ?? '', value: 'true' },
1215
+ { label: t('global.no') ?? '', value: 'false' },
1216
+ ], [t]);
1217
+
735
1218
  const endedChatsColumns = useMemo(() => {
736
1219
  const columns = [
737
1220
  columnHelper.accessor('created', {
@@ -774,7 +1257,20 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
774
1257
  },
775
1258
  {
776
1259
  id: `customerSupportFullName`,
777
- header: t('chat.history.csaName') ?? '',
1260
+ header: () => {
1261
+ return (
1262
+ <HeaderCombobox
1263
+ label={t('chat.history.csaName')}
1264
+ options={customerSupportAgentsQuery.data ?? []}
1265
+ value={csaIdCodesFilter}
1266
+ onChange={(value) => {
1267
+ const normalizedValue = normalizeCsaFilterValues(value);
1268
+ tableHeaderForm.setValue('csaIdCodesFilter', normalizedValue);
1269
+ }}
1270
+ />
1271
+ );
1272
+ },
1273
+ sortDescFirst: false,
778
1274
  }
779
1275
  ),
780
1276
  columnHelper.accessor(
@@ -784,30 +1280,134 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
784
1280
  },
785
1281
  {
786
1282
  id: 'authenticatedPerson',
787
- header: t('chat.history.authenticatedPerson') ?? '',
1283
+ header: () => {
1284
+ return (
1285
+ <HeaderCombobox
1286
+ label={t('chat.history.authenticatedPerson') ?? ''}
1287
+ options={booleanFilterOptions}
1288
+ value={getBooleanComboboxValue(showAuthenticatedPerson)}
1289
+ onChange={(value) => {
1290
+ setTableHeaderValue('showAuthenticatedPerson', getBooleanFormValue(value));
1291
+ }}
1292
+ multiple={false}
1293
+ isSearchEnabled={false}
1294
+ />
1295
+ );
1296
+ },
1297
+ sortDescFirst: false,
788
1298
  }
789
1299
  ),
1300
+ ...showEmail ? [
1301
+ columnHelper.accessor('endUserEmail', {
1302
+ id: 'endUserEmail',
1303
+ header: t('global.email'),
1304
+ sortDescFirst: false,
1305
+ })
1306
+ ] : [],
1307
+ ...testMessageEnabled ? [
1308
+ columnHelper.accessor('istest', {
1309
+ id: 'istest',
1310
+ header: () => {
1311
+ return (
1312
+ <HeaderCombobox
1313
+ label={t('global.test')}
1314
+ options={booleanFilterOptions}
1315
+ value={getBooleanComboboxValue(isTestFilter)}
1316
+ onChange={(value) => {
1317
+ setTableHeaderValue('isTestFilter', getBooleanFormValue(value));
1318
+ }}
1319
+ multiple={false}
1320
+ isSearchEnabled={false}
1321
+ />
1322
+ );
1323
+ },
1324
+ cell: markConversationAsTest,
1325
+ enableSorting: false,
1326
+ sortDescFirst: false,
1327
+ })
1328
+ ] : [],
1329
+ columnHelper.accessor('isPreserve', {
1330
+ id: 'isPreserve',
1331
+ header: () => {
1332
+ return (
1333
+ <HeaderCombobox
1334
+ label={t('global.preserve')}
1335
+ options={booleanFilterOptions}
1336
+ value={getBooleanComboboxValue(isPreserveFilter)}
1337
+ onChange={(value) => {
1338
+ setTableHeaderValue('isPreserveFilter', getBooleanFormValue(value));
1339
+ }}
1340
+ multiple={false}
1341
+ isSearchEnabled={false}
1342
+ />
1343
+ );
1344
+ },
1345
+ cell: markConversationAsPreserve,
1346
+ sortDescFirst: false,
1347
+ }),
790
1348
  columnHelper.accessor('comment', {
791
1349
  id: 'comment',
792
1350
  header: t('chat.history.comment') ?? '',
793
1351
  cell: commentView,
1352
+ enableSorting: false,
1353
+ sortDescFirst: false,
794
1354
  }),
795
1355
  columnHelper.accessor('feedbackRating', {
796
1356
  id: 'feedbackRating',
797
- header: t('chat.history.rating') ?? '',
1357
+ header: () => {
1358
+ return (
1359
+ <HeaderCombobox
1360
+ label={t('chat.history.rating') ?? ''}
1361
+ options={ratingOptions}
1362
+ value={feedbackRatings}
1363
+ onChange={(value) => {
1364
+ setTableHeaderValue(
1365
+ 'feedbackRatings',
1366
+ normalizeAllOptionFilterValues(
1367
+ value,
1368
+ feedbackRatings,
1369
+ getAllStringFilterValues(realRatingOptions)
1370
+ )
1371
+ );
1372
+ }}
1373
+ />
1374
+ );
1375
+ },
798
1376
  cell: (props) => {
799
1377
  const value = props.getValue();
800
1378
  return value !== null && value !== undefined ? <span>{`${value}/${props.row.original?.isFiveRatingScale === 'true' ? 5 : 10}`}</span> : null;
801
- }
1379
+ },
1380
+ sortDescFirst: false,
802
1381
  }),
803
1382
  columnHelper.accessor('feedbackText', {
804
1383
  id: 'feedbackText',
805
- header: t('chat.history.feedback') ?? '',
1384
+ header: t('chat.history.feedback'),
806
1385
  cell: feedbackTextView,
1386
+ enableSorting: false,
1387
+ sortDescFirst: false,
807
1388
  }),
808
1389
  columnHelper.accessor('status', {
809
1390
  id: 'status',
810
- header: t('global.status') ?? '',
1391
+ header: () => {
1392
+ return (
1393
+ <HeaderCombobox
1394
+ label={t('global.status') ?? ''}
1395
+ options={statusOptions}
1396
+ value={status}
1397
+ onChange={(value) => {
1398
+ setTableHeaderValue(
1399
+ 'status',
1400
+ normalizeAllOptionFilterValues(
1401
+ value,
1402
+ status,
1403
+ getAllStringFilterValues(realStatusOptions)
1404
+ )
1405
+ );
1406
+ }}
1407
+ isSearchEnabled={true}
1408
+ />
1409
+ );
1410
+ },
811
1411
  cell: statusView,
812
1412
  sortingFn: (a, b, isAsc) => {
813
1413
  const statusA =
@@ -825,17 +1425,118 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
825
1425
  }) * (isAsc ? 1 : -1)
826
1426
  );
827
1427
  },
1428
+ sortDescFirst: false,
828
1429
  }),
829
1430
  columnHelper.accessor('id', {
830
1431
  id: 'id',
831
1432
  header: 'ID',
832
1433
  cell: idView,
1434
+ enableSorting: false,
1435
+ sortDescFirst: false,
833
1436
  }),
834
1437
  columnHelper.accessor('endUserUrl', {
835
1438
  id: 'www',
836
- header: 'www',
1439
+ header: () => {
1440
+ return (
1441
+ <HeaderCombobox
1442
+ label={t('chat.history.www') ?? ''}
1443
+ value={domains}
1444
+ options={domainOptions}
1445
+ onChange={(value) => {
1446
+ setTableHeaderValue(
1447
+ 'domains',
1448
+ normalizeAllOptionFilterValues(value, domains, currentDomains)
1449
+ );
1450
+ }}
1451
+ />
1452
+ );
1453
+ },
837
1454
  cell: wwwView,
1455
+ sortDescFirst: false,
838
1456
  }),
1457
+ ...isChatAnalysisEnabled ? [
1458
+ columnHelper.display({
1459
+ id: 'theme',
1460
+ header: () => (
1461
+ <HeaderCombobox
1462
+ label={t('chat.history.theme')}
1463
+ options={themeOptions}
1464
+ value={theme}
1465
+ onChange={(value) => {
1466
+ setTableHeaderValue(
1467
+ 'theme',
1468
+ normalizeAllOptionFilterValues(
1469
+ value,
1470
+ theme,
1471
+ getAllStringFilterValues(realThemeOptions)
1472
+ )
1473
+ );
1474
+ }}
1475
+ />
1476
+ ),
1477
+ cell: (props) => {
1478
+ return formatChatAnalysisCell(
1479
+ props.row.original.theme,
1480
+ t('chat.quality.selectionEmptied')
1481
+ );
1482
+ },
1483
+ enableSorting: true,
1484
+ }),
1485
+ columnHelper.display({
1486
+ id: 'responseQuality',
1487
+ header: () => (
1488
+ <HeaderCombobox
1489
+ label={t('chat.history.responseQuality')}
1490
+ options={responseQualityOptions}
1491
+ value={responseQuality}
1492
+ onChange={(value) => {
1493
+ setTableHeaderValue(
1494
+ 'responseQuality',
1495
+ normalizeAllOptionFilterValues(
1496
+ value,
1497
+ responseQuality,
1498
+ getAllStringFilterValues(realResponseQualityOptions)
1499
+ )
1500
+ );
1501
+ }}
1502
+ />
1503
+ ),
1504
+ cell: (props) => {
1505
+ return formatChatAnalysisCell(
1506
+ props.row.original.responseQuality,
1507
+ t('chat.quality.selectionEmptied')
1508
+ );
1509
+ },
1510
+ enableSorting: true,
1511
+ }),
1512
+ columnHelper.display({
1513
+ id: 'followUpStatus',
1514
+ header: () => (
1515
+ <HeaderCombobox
1516
+ label={t('chat.history.followUpStatus')}
1517
+ options={followUpStatusOptions}
1518
+ value={followUpStatus}
1519
+ onChange={(value) => {
1520
+ setTableHeaderValue(
1521
+ 'followUpStatus',
1522
+ normalizeAllOptionFilterValues(
1523
+ value,
1524
+ followUpStatus,
1525
+ getAllStringFilterValues(realFollowUpStatusOptions)
1526
+ )
1527
+ );
1528
+ }}
1529
+ />
1530
+ ),
1531
+ cell: (props) => {
1532
+ return formatChatAnalysisCell(
1533
+ props.row.original.followUpStatus,
1534
+ t('chat.quality.selectionEmptied')
1535
+ );
1536
+ },
1537
+ enableSorting: true,
1538
+ }),
1539
+ ] : [],
839
1540
  columnHelper.display({
840
1541
  id: 'detail',
841
1542
  cell: detailsView,
@@ -846,29 +1547,37 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
846
1547
  }),
847
1548
  ];
848
1549
 
849
- if (showEmail) {
850
- columns.splice(4, 0, columnHelper.accessor('endUserEmail', {
851
- id: 'endUserEmail',
852
- header: t('global.email') ?? '',
853
- }));
854
- }
855
-
856
- if (testMessageEnabled) {
857
- columns.splice(4, 0, columnHelper.accessor('istest', {
858
- id: 'istest',
859
- header: t('global.test') ?? '',
860
- cell: markConversationAsTest
861
- }));
862
- }
863
-
864
- columns.splice(4, 0, columnHelper.accessor('isPreserve', {
865
- id: 'isPreserve',
866
- header: t('global.preserve') ?? '',
867
- cell: markConversationAsPreserve
868
- }));
869
-
870
1550
  return columns;
871
- }, [t, showEmail, testMessageEnabled])
1551
+ }, [
1552
+ t,
1553
+ showEmail,
1554
+ testMessageEnabled,
1555
+ customerSupportAgentsQuery.data,
1556
+ csaIdCodesFilter,
1557
+ feedbackRatings,
1558
+ showAuthenticatedPerson,
1559
+ isTestFilter,
1560
+ isPreserveFilter,
1561
+ domains,
1562
+ status,
1563
+ theme,
1564
+ responseQuality,
1565
+ followUpStatus,
1566
+ statusOptions,
1567
+ realStatusOptions,
1568
+ themeOptions,
1569
+ responseQualityOptions,
1570
+ followUpStatusOptions,
1571
+ realThemeOptions,
1572
+ realResponseQualityOptions,
1573
+ realFollowUpStatusOptions,
1574
+ currentDomains,
1575
+ ratingOptions,
1576
+ realRatingOptions,
1577
+ domainOptions,
1578
+ booleanFilterOptions,
1579
+ isChatAnalysisEnabled,
1580
+ ])
872
1581
 
873
1582
  const getSortingString = () => {
874
1583
  if (sorting && sorting.length > 0) {
@@ -904,6 +1613,12 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
904
1613
  return t('chat.history.rating') ?? ''
905
1614
  case 'feedbackText':
906
1615
  return t('chat.history.feedback') ?? ''
1616
+ case 'theme':
1617
+ return t('chat.history.theme') ?? ''
1618
+ case 'responseQuality':
1619
+ return t('chat.history.responseQuality') ?? ''
1620
+ case 'followUpStatus':
1621
+ return t('chat.history.followUpStatus') ?? ''
907
1622
  case 'status':
908
1623
  return t('global.status') ?? ''
909
1624
  case 'endUserUrl':
@@ -1032,15 +1747,10 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
1032
1747
  return {headers, rows, chatIds};
1033
1748
  };
1034
1749
 
1035
-
1036
1750
  const downloadChatHistory = async () => {
1037
1751
  setLoading(true);
1038
1752
  try {
1039
- let sortBy = 'created desc';
1040
- if (sorting.length > 0) {
1041
- const sortType = sorting[0].desc ? 'desc' : 'asc';
1042
- sortBy = `${sorting[0].id} ${sortType}`;
1043
- }
1753
+ const sortBy = getEndedChatsSortBy(sorting);
1044
1754
 
1045
1755
  const realSelectedColumns = getRealSelectedColumns(selectedColumns);
1046
1756
  const { headers } = mapChatsToExportRows([], endedChatsColumns, realSelectedColumns, t);
@@ -1061,7 +1771,7 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
1061
1771
  language: i18n.language,
1062
1772
  startDate: formatISO(startOfDay(new Date(startDate))),
1063
1773
  endDate: formatISO(endOfDay(new Date(endDate))),
1064
- urls: getDomainsArray(currentDomains),
1774
+ ...endedChatsFilterBody,
1065
1775
  sorting: sortBy,
1066
1776
  search,
1067
1777
  });
@@ -1079,27 +1789,151 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
1079
1789
  }
1080
1790
  };
1081
1791
 
1082
- const endUserFullName = getUserName();
1083
-
1084
- const isClearFiltersVisible = useMemo(()=> {
1085
- return search.length > 0 || selectedColumns.length > 0;
1086
- }, [search, selectedColumns]);
1087
-
1088
1792
  const onClearFilersClick = () => {
1089
- const clearedColumns: string[] = [];
1090
- setSelectedColumns(clearedColumns);
1091
1793
  setCounterKey(0);
1092
1794
  setValue('search', '');
1093
- updatePagePreferences.mutate({
1094
- page_results: pagination.pageSize,
1095
- selected_columns: clearedColumns
1795
+ resetTableHeaderForm({
1796
+ csaIdCodesFilter: [],
1797
+ feedbackRatings: [],
1798
+ showAuthenticatedPerson: undefined,
1799
+ isTestFilter: undefined,
1800
+ isPreserveFilter: undefined,
1801
+ domains: [],
1802
+ status: [],
1803
+ theme: [],
1804
+ responseQuality: [],
1805
+ followUpStatus: [],
1096
1806
  });
1097
1807
  };
1098
1808
 
1809
+ const chatQualityMeasurementQuery = useQuery({
1810
+ queryKey: ['chats/quality/measurements', selectedChat?.id],
1811
+ queryFn: () => getQualityMeasurements({ chatUuid: selectedChat!.id }),
1812
+ enabled: !!selectedChat?.id,
1813
+ });
1814
+
1815
+ const selectedQualityMeasurements = useMemo(() => {
1816
+ const measurements = chatQualityMeasurementQuery.data?.data.response ?? [];
1817
+
1818
+ const latest = {
1819
+ theme: {
1820
+ time: Number.NEGATIVE_INFINITY,
1821
+ values: [] as string[],
1822
+ },
1823
+ quality: {
1824
+ time: Number.NEGATIVE_INFINITY,
1825
+ value: '',
1826
+ },
1827
+ followUp: {
1828
+ time: Number.NEGATIVE_INFINITY,
1829
+ value: '',
1830
+ },
1831
+ };
1832
+
1833
+ measurements.forEach(({ type, value, createdAt }) => {
1834
+ const parsedTime = Date.parse(createdAt);
1835
+ const time = Number.isNaN(parsedTime) ? 0 : parsedTime;
1836
+
1837
+ if (type === MEASUREMENT_TYPES.THEME) {
1838
+ if (time > latest.theme.time) {
1839
+ latest.theme.time = time;
1840
+ latest.theme.values = value ? [value] : [];
1841
+ } else if (time === latest.theme.time && value) {
1842
+ latest.theme.values.push(value);
1843
+ }
1844
+
1845
+ return;
1846
+ }
1847
+
1848
+
1849
+ if (type === MEASUREMENT_TYPES.QUALITY && time >= latest.quality.time) {
1850
+ latest.quality.time = time;
1851
+ latest.quality.value = value;
1852
+ return;
1853
+ }
1854
+
1855
+ if (type === MEASUREMENT_TYPES.FOLLOW_UP_ACTION && time >= latest.followUp.time) {
1856
+ latest.followUp.time = time;
1857
+ latest.followUp.value = value;
1858
+ return;
1859
+ }
1860
+ });
1861
+
1862
+ return {
1863
+ theme: latest.theme.values,
1864
+ quality: latest.quality.value,
1865
+ followUp: latest.followUp.value,
1866
+ };
1867
+ }, [chatQualityMeasurementQuery.data]);
1868
+
1869
+ const chatQualityMeasurementMutation = useMutation({
1870
+ mutationFn: postQualityMeasurements,
1871
+ onSuccess: (_data, variables) => {
1872
+ queryClient.invalidateQueries({
1873
+ queryKey: ['chats/quality/measurements', variables.chatUuid],
1874
+ });
1875
+ },
1876
+ onError: (error: AxiosError) => {
1877
+ toast?.open({
1878
+ type: 'error',
1879
+ title: t('global.notificationError'),
1880
+ message: error.message,
1881
+ });
1882
+ },
1883
+ });
1884
+
1885
+ const saveChatQualityMeasurement = async (
1886
+ type: MeasurementType,
1887
+ value: string | string[],
1888
+ successMessage: string
1889
+ ) => {
1890
+ if (!selectedChat?.id) return;
1891
+
1892
+ try {
1893
+ await chatQualityMeasurementMutation.mutateAsync({
1894
+ chatUuid: selectedChat.id,
1895
+ type,
1896
+ value,
1897
+ });
1898
+
1899
+ toast?.open({
1900
+ type: 'success',
1901
+ title: t('global.notification'),
1902
+ message: successMessage,
1903
+ });
1904
+ } catch {
1905
+ // Error toast is handled by the mutation's onError callback.
1906
+ }
1907
+ };
1908
+
1909
+ const onChatThemeChange = async (value: string[]) => {
1910
+ // TODO: array support in backend
1911
+ saveChatQualityMeasurement(
1912
+ MEASUREMENT_TYPES.THEME,
1913
+ value,
1914
+ t('toast.success.conversationThemeSaved')
1915
+ )
1916
+ }
1917
+
1918
+ const onChatQualityChange = async (value: string) => {
1919
+ await saveChatQualityMeasurement(
1920
+ MEASUREMENT_TYPES.QUALITY,
1921
+ value,
1922
+ t('toast.success.conversationQualitySaved')
1923
+ );
1924
+ }
1925
+ const onChatFollowUpChange = async (value: string) => {
1926
+ await saveChatQualityMeasurement(
1927
+ MEASUREMENT_TYPES.FOLLOW_UP_ACTION,
1928
+ value,
1929
+ t('toast.success.conversationFollowUpActionSaved')
1930
+ );
1931
+ }
1932
+
1099
1933
  if (!filteredEndedChatsList) return <>Loading... {{filteredEndedChatsList}} something is wrong </>;
1100
1934
 
1101
1935
  return (
1102
- <>
1936
+ <div className="history-page-wrapper">
1103
1937
  <div className="header-container">
1104
1938
  {displayTitle && (
1105
1939
  <h1>{t('chat.history.title')}{totalCount === null ? '' : ` (${totalCount.toLocaleString('et-EE')})`}</h1>
@@ -1250,12 +2084,23 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
1250
2084
  </Button>
1251
2085
  </div>)
1252
2086
  }
1253
- {isClearFiltersVisible && (
1254
- <Track justify="between" style={{ marginBottom: '16px' }}>
1255
- <ClearFiltersButton style={{ marginLeft: 'auto' }} onClick={onClearFilersClick} />
1256
- </Track>
1257
- )}
1258
- <div className="card-drawer-container" style={{height: '100%', overflow: 'auto'}}>
2087
+ <SelectedFilterTags
2088
+ csaFilterTagValues={csaFilterTagValues}
2089
+ getCsaFilterTagLabel={getCsaFilterTagLabel}
2090
+ showAuthenticatedPerson={showAuthenticatedPerson}
2091
+ showTestFilter={testMessageEnabled}
2092
+ isTestFilter={isTestFilter}
2093
+ isPreserveFilter={isPreserveFilter}
2094
+ domains={domainFilterValues}
2095
+ feedbackRatings={feedbackRatingFilterValues}
2096
+ status={statusFilterValues}
2097
+ theme={themeFilterValues}
2098
+ responseQuality={responseQualityFilterValues}
2099
+ followUpStatus={followUpStatusFilterValues}
2100
+ onRemove={removeSelectedFilterTag}
2101
+ onClearFiltersClick={onClearFilersClick}
2102
+ />
2103
+ <div className="card-drawer-container" style={{height: '100%', overflow: 'auto', maxHeight: '60vh'}}>
1259
2104
  <div className="card-wrapper">
1260
2105
  <Card>
1261
2106
  <DataTable
@@ -1329,141 +2174,37 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
1329
2174
  />
1330
2175
  </Drawer>
1331
2176
  </div>
1332
- <div className="side-meta">
1333
- <div>
1334
- <p>
1335
- <strong>ID</strong>
1336
- </p>
1337
- <p>{selectedChat.id}</p>
1338
- </div>
1339
- <div>
1340
- <p>
1341
- <strong>{t('chat.endUser')}</strong>
1342
- </p>
1343
- <p>{endUserFullName}</p>
1344
- </div>
1345
- {selectedChat.endUserId && (
1346
- <div>
1347
- <p>
1348
- <strong>{t('chat.endUserId')}</strong>
1349
- </p>
1350
- <p>{selectedChat.endUserId ?? ''}</p>
1351
- </div>
1352
- )}
1353
- {selectedChat.endUserEmail && (
1354
- <div>
1355
- <p>
1356
- <strong>{t('chat.endUserEmail')}</strong>
1357
- </p>
1358
- <p>{selectedChat.endUserEmail}</p>
1359
- </div>
1360
- )}
1361
- {selectedChat.endUserPhone && (
1362
- <div>
1363
- <p>
1364
- <strong>{t('chat.endUserPhoneNumber')}</strong>
1365
- </p>
1366
- <p>{selectedChat.endUserPhone}</p>
1367
- </div>
1368
- )}
1369
- {selectedChat.customerSupportDisplayName && (
1370
- <div>
1371
- <p>
1372
- <strong>{t('chat.csaName')}</strong>
1373
- </p>
1374
- <p>{selectedChat.customerSupportDisplayName}</p>
1375
- </div>
1376
- )}
1377
- <div>
1378
- <p>
1379
- <strong>{t('chat.startedAt')}</strong>
1380
- </p>
1381
- <p>
1382
- {format(
1383
- new Date(selectedChat.created),
1384
- 'dd. MMMM Y HH:mm:ss',
1385
- {
1386
- locale: et,
1387
- }
1388
- ).toLowerCase()}
1389
- </p>
1390
- </div>
1391
- <div>
1392
- <p>
1393
- <strong>{t('chat.device')}</strong>
1394
- </p>
1395
- <p>{selectedChat.endUserOs}</p>
1396
- </div>
1397
- <div>
1398
- <p>
1399
- <strong>{t('chat.location')}</strong>
1400
- </p>
1401
- <p>{selectedChat.endUserUrl}</p>
1402
- </div>
1403
- {selectedChat.comment && (
1404
- <div>
1405
- <p>
1406
- <strong>{t('chat.history.comment')}</strong>
1407
- </p>
1408
- <p>{selectedChat.comment}</p>
1409
- </div>
1410
- )}
1411
- {selectedChat.commentAuthor && (
1412
- <div>
1413
- <p>
1414
- <strong>{t('chat.history.commentAuthor')}</strong>
1415
- </p>
1416
- <p>{selectedChat.commentAuthor}</p>
1417
- </div>
1418
- )}
1419
- {selectedChat.commentAddedDate && (
1420
- <div>
1421
- <p>
1422
- <strong>{t('chat.history.commentAddedDate')}</strong>
1423
- </p>
1424
- <p>
1425
- {format(
1426
- new Date(selectedChat.commentAddedDate),
1427
- 'dd.MM.yyyy'
1428
- )}
1429
- </p>
1430
- </div>
1431
- )}
1432
- {selectedChat.lastMessageEvent && (
1433
- <div>
1434
- <p>
1435
- <strong>{t('global.status')}</strong>
1436
- </p>
1437
- <p>
1438
- {t('chat.plainEvents.' + selectedChat.lastMessageEvent)}
1439
- </p>
1440
- </div>
1441
- )}
1442
- {selectedChat.userDisplayName && (
1443
- <div>
1444
- <p>
1445
- <strong>{t('chat.history.statusAdder')}</strong>
1446
- </p>
1447
- <p>{selectedChat.userDisplayName}</p>
1448
- </div>
1449
- )}
1450
- {selectedChat.lastMessageTimestamp && (
1451
- <div>
1452
- <p>
1453
- <strong>{t('chat.history.statusAddedDate')}</strong>
1454
- </p>
1455
- <p>
1456
- {format(
1457
- new Date(selectedChat.lastMessageTimestamp),
1458
- 'dd.MM.yyyy'
1459
- )}
1460
- </p>
1461
- </div>
1462
- )}
1463
- </div>
1464
- </>
1465
- )}
1466
- </div>
2177
+ <ChatMetadataPanel
2178
+ chat={selectedChat}
2179
+ chatMeasurments={chatQualityMeasurementQuery.data?.data.response ?? []}
2180
+ >
2181
+ <QualitySettings
2182
+ theme={{
2183
+ onChange: (value) => onChatThemeChange(value),
2184
+ options: qualitySettingsConfigQuery.data?.chatAnalysisTheme.map(item => {
2185
+ return { label: item, value: item }
2186
+ }) ?? [],
2187
+ value: selectedQualityMeasurements.theme,
2188
+ }}
2189
+ quality={{
2190
+ onChange: (value) => onChatQualityChange(value),
2191
+ options: qualitySettingsConfigQuery.data?.chatAnalysisBykResponseQuality.map(item => {
2192
+ return { label: item, value: item }
2193
+ }) ?? [],
2194
+ value: selectedQualityMeasurements.quality,
2195
+ }}
2196
+ followUp={{
2197
+ onChange: (value) => onChatFollowUpChange(value),
2198
+ options: qualitySettingsConfigQuery.data?.chatAnalysisFollowUpAction.map(item => {
2199
+ return { label: item, value: item }
2200
+ }) ?? [],
2201
+ value: selectedQualityMeasurements.followUp,
2202
+ }}
2203
+ />
2204
+ </ChatMetadataPanel>
2205
+ </>
2206
+ )}
2207
+ </div>
1467
2208
  </div>
1468
2209
 
1469
2210
  {statusChangeModal && (
@@ -1496,15 +2237,8 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
1496
2237
  <p>{t('global.removeValidation')}</p>
1497
2238
  </Dialog>
1498
2239
  )}
1499
- </>
2240
+ </div>
1500
2241
  );
1501
-
1502
- function getUserName() {
1503
- return selectedChat?.endUserFirstName !== '' &&
1504
- selectedChat?.endUserLastName !== ''
1505
- ? `${selectedChat?.endUserFirstName} ${selectedChat?.endUserLastName}`
1506
- : t('global.anonymous');
1507
- }
1508
2242
  };
1509
2243
 
1510
2244
  export default ChatHistory;