@buerokratt-ria/common-gui-components 0.0.58 → 0.0.60

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