@buerokratt-ria/common-gui-components 0.0.24 → 0.0.26

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All changes to this project will be documented in this file.
4
4
 
5
5
  ## Template [MajorVersion.MediterraneanVersion.MinorVersion] - DD-MM-YYYY
6
6
 
7
+ ## [0.0.26] - 15-09-2025
8
+
9
+ - Added download button do download chat history with currently selected criteriasa
10
+ - Added new optional param to enable this button(disabled by default)
11
+
12
+ ## [0.0.25] - 01-09-2025
13
+
14
+ - Added test column to display chats mark for test
15
+ - FIX: refetch based on updated domains
16
+
7
17
  ## [0.0.24] - 14-08-2025
8
18
 
9
19
  - Modified Start and End dates to send them in payload in iso format
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buerokratt-ria/common-gui-components",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "Common GUI components and pre defined templates.",
5
5
  "main": "index.ts",
6
6
  "author": "ExiRai",
@@ -29,6 +29,7 @@
29
29
  "downshift": "^7.0.5",
30
30
  "esbuild": "^0.19.5",
31
31
  "framer-motion": "^8.5.5",
32
+ "file-saver": "^2.0.5",
32
33
  "i18next": "^22.4.5",
33
34
  "i18next-browser-languagedetector": "^7.0.1",
34
35
  "linkify-react": "^4.1.1",
@@ -0,0 +1,31 @@
1
+ import { Buffer } from 'buffer';
2
+
3
+ export const saveFile = async (base64String: string, fileName: `${string}.${string}`, type: string) => {
4
+ const blob = new Blob([Buffer.from(base64String, 'base64')], {
5
+ type: type,
6
+ });
7
+
8
+ const extension = fileName.split('.').pop();
9
+
10
+ if (window.showSaveFilePicker) {
11
+ const handle = await window.showSaveFilePicker({
12
+ suggestedName: fileName,
13
+ types: [
14
+ {
15
+ description: extension!.toUpperCase() + ' file',
16
+ accept: { [type]: [`.${extension}` as `.${string}`] },
17
+ },
18
+ ],
19
+ });
20
+ const writable = await handle.createWritable();
21
+ await writable.write(blob);
22
+ writable.close();
23
+ } else {
24
+ const url = window.URL.createObjectURL(blob);
25
+ const a = document.createElement('a');
26
+ a.href = url;
27
+ a.download = fileName;
28
+ a.click();
29
+ window.URL.revokeObjectURL(url);
30
+ }
31
+ };
@@ -1,5 +1,6 @@
1
1
  @import 'src/styles/tools/spacing';
2
2
  @import 'src/styles/tools/color';
3
+ @import 'src/styles/settings/variables/other';
3
4
  @import 'src/styles/settings/variables/typography';
4
5
 
5
6
  .input-wrapper {
@@ -13,6 +14,39 @@
13
14
  box-sizing: border-box;
14
15
  }
15
16
 
17
+ .checkbox-test {
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+
22
+ &__item {
23
+ input[type=checkbox] {
24
+ &::before {
25
+ content: '';
26
+ display: block;
27
+ width: 16px;
28
+ height: 16px;
29
+ box-shadow: inset 0 0 0 1px get-color(black-coral-2);
30
+ border-radius: 2px;
31
+ position: absolute;
32
+ left: 4px;
33
+ top: 4px;
34
+ }
35
+ }
36
+
37
+ &:checked {
38
+ &::before {
39
+ background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTEiIHZpZXdCb3g9IjAgMCAxNCAxMSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuNzQ5NzkgOC4xMjkwNkwxLjYyMjI5IDUuMDAxNTZMMC41NjEwMzUgNi4wNjI4MUw0Ljc0OTc5IDEwLjI1MTZMMTMuNzQ5OCAxLjI1MTU2TDEyLjY4ODUgMC4xOTAzMDhMNC43NDk3OSA4LjEyOTA2WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==');
40
+ background-color: get-color(sapphire-blue-10);
41
+ background-repeat: no-repeat;
42
+ background-position: center;
43
+ background-size: 13px 10px;
44
+ box-shadow: inset 0 0 0 1px get-color(sapphire-blue-10);
45
+ }
46
+ }
47
+ }
48
+ }
49
+
16
50
  .card-wrapper {
17
51
  flex: 1;
18
52
  overflow: auto;
@@ -1,11 +1,12 @@
1
- import React, {FC, PropsWithChildren, useEffect, useRef, useMemo, useState} from 'react';
1
+ import React, {FC, PropsWithChildren, useEffect, useMemo, useRef, useState} from 'react';
2
2
  import {useTranslation} from 'react-i18next';
3
3
  import {useMutation} from '@tanstack/react-query';
4
4
  import {ColumnPinningState, createColumnHelper, PaginationState, SortingState,} from '@tanstack/react-table';
5
- import { format, startOfDay, endOfDay, formatISO } from "date-fns";
5
+ import {endOfDay, format, formatISO, startOfDay} from "date-fns";
6
6
  import {AxiosError} from 'axios';
7
7
  import './History.scss';
8
8
  import {MdOutlineRemoveRedEye} from 'react-icons/md';
9
+ import {CgSpinner} from 'react-icons/cg';
9
10
 
10
11
  import {
11
12
  Button,
@@ -13,6 +14,7 @@ import {
13
14
  DataTable,
14
15
  Dialog,
15
16
  Drawer,
17
+ FormCheckbox,
16
18
  FormDatepicker,
17
19
  FormInput,
18
20
  FormMultiselect,
@@ -34,6 +36,7 @@ import {ToastContextType} from "../../../context";
34
36
 
35
37
  import {getDomainsArray} from "../../../utils/multiDomain-utils";
36
38
  import {StoreState} from "../../../store";
39
+ import {saveFile} from "../../../services/file";
37
40
 
38
41
  type HistoryProps = {
39
42
  user: UserInfo | null;
@@ -41,6 +44,7 @@ type HistoryProps = {
41
44
  toastContext: ToastContextType | null;
42
45
  onMessageClick?: (message: any) => void;
43
46
  showComment?: boolean;
47
+ showDownload?: boolean;
44
48
  showEmail?: boolean;
45
49
  showSortingLabel?: boolean;
46
50
  showStatus?: boolean;
@@ -51,12 +55,19 @@ type HistoryProps = {
51
55
  delegatedEndDate?: string;
52
56
  }
53
57
 
58
+ type ExportResult = {
59
+ headers: string[];
60
+ rows: (string | number | null)[][];
61
+ chatIds: string[];
62
+ };
63
+
54
64
  const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
55
65
  user,
56
66
  userDomains,
57
67
  toastContext,
58
68
  onMessageClick,
59
69
  showComment = true,
70
+ showDownload = false,
60
71
  showEmail = false,
61
72
  showSortingLabel = false,
62
73
  showStatus = true,
@@ -109,6 +120,8 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
109
120
  const [updateKey, setUpdateKey] = useState<number>(0)
110
121
  const currentDomains = useStore.getState().userDomains;
111
122
  const multiDomainEnabled = import.meta.env.REACT_APP_ENABLE_MULTI_DOMAIN?.toLowerCase() === 'true';
123
+ const testMessageEnabled = import.meta.env.REACT_APP_SHOW_TEST_MESSAGE?.toLowerCase() === 'true';
124
+ const [loading, setLoading] = useState(false);
112
125
 
113
126
  const parseDateParam = (dateString: string | null) => {
114
127
  if (!dateString) return new Date();
@@ -143,9 +156,9 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
143
156
  });
144
157
  }, 500);
145
158
 
146
- if(multiDomainEnabled) {
159
+ if (multiDomainEnabled) {
147
160
  useStore.subscribe((state, prevState) => {
148
- if(JSON.stringify(state.userDomains) !== JSON.stringify(prevState.userDomains)) {
161
+ if (JSON.stringify(state.userDomains) !== JSON.stringify(prevState.userDomains)) {
149
162
  setUpdateKey(prevState => prevState + 1);
150
163
  }
151
164
  });
@@ -170,7 +183,7 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
170
183
  setValue('endDate', unifyDateFromat(delegatedEndDate));
171
184
  }
172
185
 
173
- if(initialLoad) {
186
+ if (initialLoad) {
174
187
  fetchData()
175
188
  } else {
176
189
  getAllEndedChats.mutate({
@@ -248,7 +261,7 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
248
261
  search,
249
262
  });
250
263
  }
251
- }, [selectedColumns]);
264
+ }, [selectedColumns, currentDomains]);
252
265
 
253
266
  useEffect(() => {
254
267
  listCustomerSupportAgents.mutate();
@@ -356,11 +369,15 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
356
369
  ];
357
370
 
358
371
  if (showEmail) {
359
- columns.splice(4, 0, {label: t('global.email'), value: 'endUserEmail'}); // insert after name
372
+ columns.splice(4, 0, {label: t('global.email'), value: 'endUserEmail'});
373
+ }
374
+
375
+ if (testMessageEnabled) {
376
+ columns.splice(5, 0, {label: t('global.test'), value: 'istest'});
360
377
  }
361
378
 
362
379
  return columns;
363
- }, [t, showEmail])
380
+ }, [t, showEmail, testMessageEnabled])
364
381
 
365
382
  const chatStatusChangeMutation = useMutation({
366
383
  mutationFn: async (data: { chatId: string | number; event: string }) => {
@@ -451,6 +468,31 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
451
468
  },
452
469
  });
453
470
 
471
+ const chatTestChangeMutation = useMutation({
472
+ mutationFn: (data: {
473
+ chatId: string | number;
474
+ isTest: boolean;
475
+ }) => apiDev.post('chats/mark-test', data),
476
+ onSuccess: (res, {chatId, isTest}) => {
477
+ const updatedChatList = filteredEndedChatsList.map((chat) =>
478
+ chat.id === chatId ? {...chat, isTest} : chat
479
+ );
480
+ filterChatsList(updatedChatList);
481
+ toast?.open({
482
+ type: 'success',
483
+ title: t('global.notification'),
484
+ message: t('toast.success.updateSuccess'),
485
+ });
486
+ },
487
+ onError: (error: AxiosError) => {
488
+ toast?.open({
489
+ type: 'error',
490
+ title: t('global.notificationError'),
491
+ message: error.message,
492
+ });
493
+ },
494
+ });
495
+
454
496
  const columnHelper = createColumnHelper<ChatType>();
455
497
 
456
498
  const copyValueToClipboard = async (value: string) => {
@@ -520,6 +562,26 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
520
562
  </Tooltip>
521
563
  );
522
564
 
565
+ const markConversationAsTest = (props: any) => {
566
+ return <>
567
+ <FormCheckbox
568
+ label={''}
569
+ hideLabel
570
+ emptyItem={true}
571
+ name="active"
572
+
573
+ item={{
574
+ label: '',
575
+ value: 'active',
576
+ checked: props.getValue()
577
+ }}
578
+ onChange={(e) => {
579
+ return chatTestChangeMutation.mutate({chatId: props.row.original.id, isTest: e.target.checked})
580
+ }}
581
+ />
582
+ </>
583
+ }
584
+
523
585
  const detailsView = (props: any) => (
524
586
  <Button
525
587
  appearance="text"
@@ -574,8 +636,8 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
574
636
  }
575
637
  },
576
638
  {
577
- id: `customerSupportFullName`,
578
- header: t('chat.history.csaName') ?? '',
639
+ id: `customerSupportFullName`,
640
+ header: t('chat.history.csaName') ?? '',
579
641
  }
580
642
  ),
581
643
  columnHelper.accessor(
@@ -660,11 +722,19 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
660
722
  }));
661
723
  }
662
724
 
725
+ if (testMessageEnabled) {
726
+ columns.splice(4, 0, columnHelper.accessor('istest', {
727
+ id: 'istest',
728
+ header: t('global.test') ?? '',
729
+ cell: markConversationAsTest
730
+ }));
731
+ }
732
+
663
733
  return columns;
664
- }, [t, showEmail])
734
+ }, [t, showEmail, testMessageEnabled])
665
735
 
666
736
  const getSortingString = () => {
667
- if(sorting && sorting.length > 0) {
737
+ if (sorting && sorting.length > 0) {
668
738
  const sortingObject = sorting[0];
669
739
  const sortingString = t('sorting.sorting');
670
740
  const orderingString = t(`sorting.${sortingObject.desc ? 'desc' : 'asc'}`);
@@ -675,7 +745,7 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
675
745
  }
676
746
  }
677
747
 
678
- const getColumnTranslation = (column: string) : string => {
748
+ const getColumnTranslation = (column: string): string => {
679
749
  switch (column) {
680
750
  case 'endUserId':
681
751
  return t('global.idCode') ?? ''
@@ -703,6 +773,8 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
703
773
  return 'www'
704
774
  case 'id':
705
775
  return 'id';
776
+ case 'istest':
777
+ return t('global.test') ?? ''
706
778
  default:
707
779
  return '';
708
780
  }
@@ -749,6 +821,120 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
749
821
  );
750
822
  };
751
823
 
824
+ const mapChatsToExportRows = (
825
+ chats: ChatType[],
826
+ allColumns: any[],
827
+ selectedColumns: string[],
828
+ t: (key: string) => string
829
+ ): ExportResult => {
830
+ const activeColumns =
831
+ selectedColumns.length > 0
832
+ ? allColumns.filter(
833
+ (col) => col.id && col.id !== 'detail' && selectedColumns.includes(col.id)
834
+ )
835
+ : allColumns.filter((col) => col.id && col.id !== 'detail');
836
+
837
+ const headers = activeColumns.map(
838
+ (col) => getColumnTranslation(col.id) || col.header || col.id
839
+ );
840
+
841
+ const rows = chats.map((chat) =>
842
+ activeColumns.map((col) => {
843
+ let rawValue: any = null;
844
+
845
+ if (typeof col.accessorFn === 'function') {
846
+ rawValue = col.accessorFn(chat, 0);
847
+ } else if (typeof col.accessorKey === 'string') {
848
+ rawValue = (chat as any)[col.accessorKey];
849
+ }
850
+
851
+ let processedValue: any = rawValue;
852
+ switch (col.id) {
853
+ case 'created':
854
+ case 'ended':
855
+ processedValue = rawValue
856
+ ? format(
857
+ new Date(rawValue),
858
+ 'dd.MM.yyyy HH:mm:ss',
859
+ i18n.language === 'et' ? {locale: et} : undefined
860
+ )
861
+ : '';
862
+ break;
863
+ case 'contactsMessage':
864
+ processedValue = rawValue ? t('global.yes') : t('global.no');
865
+ break;
866
+ case 'feedbackRating':
867
+ processedValue = rawValue != null ? `${rawValue}/10` : '';
868
+ break;
869
+ case 'status':
870
+ processedValue =
871
+ chat.status === CHAT_STATUS.ENDED
872
+ ? t('chat.plainEvents.' + (chat.lastMessageEvent ?? ''))
873
+ : '';
874
+ break;
875
+ case 'endUserName':
876
+ processedValue = `${chat.endUserFirstName ?? ''} ${chat.endUserLastName ?? ''}`;
877
+ break;
878
+ default:
879
+ processedValue =
880
+ typeof rawValue === 'object' ? JSON.stringify(rawValue) : rawValue ?? '';
881
+ }
882
+
883
+ return processedValue;
884
+ })
885
+ );
886
+
887
+ const chatIds = chats.map((c) => c.id);
888
+
889
+ return {headers, rows, chatIds};
890
+ };
891
+
892
+
893
+ const downloadChatHistory = async () => {
894
+ setLoading(true);
895
+ try {
896
+ let sortBy = 'created desc';
897
+ if (sorting.length > 0) {
898
+ const sortType = sorting[0].desc ? 'desc' : 'asc';
899
+ sortBy = `${sorting[0].id} ${sortType}`;
900
+ }
901
+
902
+ const chats = await apiDev.post('agents/chats/ended', {
903
+ customerSupportIds: passedCustomerSupportIds,
904
+ startDate: formatISO(startOfDay(new Date(startDate))),
905
+ endDate: formatISO(endOfDay(new Date(endDate))),
906
+ urls: getDomainsArray(currentDomains),
907
+ page: 1,
908
+ page_size: 1000,
909
+ sorting: sortBy,
910
+ search,
911
+ });
912
+
913
+ const {headers, rows, chatIds} = mapChatsToExportRows(
914
+ chats.data.response,
915
+ endedChatsColumns,
916
+ selectedColumns,
917
+ t
918
+ );
919
+
920
+ const response = await apiDev.post('chats/ended/download', {
921
+ headers, rows, chatIds
922
+ });
923
+
924
+
925
+ await saveFile(
926
+ response.data.base64String,
927
+ 'history.xlsx',
928
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
929
+ );
930
+
931
+ } catch (error) {
932
+ console.error('Error getting CSV file:', error);
933
+ } finally {
934
+ setLoading(false);
935
+ }
936
+ };
937
+
752
938
  const endUserFullName = getUserName();
753
939
 
754
940
  if (!filteredEndedChatsList) return <>Loading... {{filteredEndedChatsList}} something is wrong </>;
@@ -759,6 +945,15 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
759
945
  <h1>{t('chat.history.title')}</h1>
760
946
  )}
761
947
 
948
+ {showDownload && (
949
+ <div>
950
+ <Button appearance={"primary"} onClick={downloadChatHistory}>
951
+ {loading && <CgSpinner className="spinner"/>}
952
+ {!loading && t('files.download_xlsx')}
953
+ </Button>
954
+ </div>
955
+ )}
956
+
762
957
  <Card>
763
958
  <Track gap={16}>
764
959
  {displaySearchBar && (
@@ -902,7 +1097,7 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
902
1097
  </Button>
903
1098
  </div>)
904
1099
  }
905
- <div className="card-drawer-container" style={{ height: '100%', overflow: 'auto' }}>
1100
+ <div className="card-drawer-container" style={{height: '100%', overflow: 'auto'}}>
906
1101
  <div className="card-wrapper">
907
1102
  <Card>
908
1103
  <DataTable
@@ -17,6 +17,8 @@
17
17
  }
18
18
 
19
19
  &__item {
20
+ position: relative;
21
+
20
22
  input[type=checkbox] {
21
23
  display: none;
22
24
 
@@ -53,5 +55,18 @@
53
55
  }
54
56
  }
55
57
  }
58
+
59
+ &--nolabel {
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ width: 100%;
64
+
65
+ &::before {
66
+ position: static !important;
67
+ transform: none !important;
68
+ margin: 0;
69
+ }
70
+ }
56
71
  }
57
- }
72
+ }
@@ -6,6 +6,7 @@ type FormCheckboxType = InputHTMLAttributes<HTMLInputElement> & {
6
6
  label: string;
7
7
  name: string;
8
8
  hideLabel?: boolean;
9
+ emptyItem?: boolean;
9
10
  item: {
10
11
  label: string;
11
12
  value: string;
@@ -18,6 +19,7 @@ const FormCheckbox = forwardRef<HTMLInputElement, FormCheckboxType>((
18
19
  label,
19
20
  name,
20
21
  hideLabel,
22
+ emptyItem,
21
23
  item,
22
24
  ...rest
23
25
  },
@@ -28,9 +30,13 @@ const FormCheckbox = forwardRef<HTMLInputElement, FormCheckboxType>((
28
30
  return (
29
31
  <div className='checkbox'>
30
32
  {label && !hideLabel && <label className='checkbox__label'>{label}</label>}
31
- <div className='checkbox__item'>
33
+ <div className={`checkbox__item ${!emptyItem ? "checkbox__item--nolabel" : ""}`}>
32
34
  <input ref={ref} type='checkbox' name={name} id={uid} value={item.value} defaultChecked={item.checked} {...rest} />
33
- <label htmlFor={uid}>{item.label}</label>
35
+ {emptyItem ? (
36
+ <label htmlFor={uid} className="checkbox__item--nolabel" />
37
+ ) : (
38
+ <label htmlFor={uid}>{item.label}</label>
39
+ )}
34
40
  </div>
35
41
  </div>
36
42
  );
@@ -1,7 +1,5 @@
1
- import useStore from "../store";
2
-
3
1
  export const getDomainsArray = (currentDomains) => {
4
- const multiDomainEnabled = import.meta.env.REACT_APP_ENABLE_MULTI_DOMAIN.toLowerCase() === 'true';
2
+ const multiDomainEnabled = import.meta.env.REACT_APP_ENABLE_MULTI_DOMAIN?.toLowerCase() === 'true';
5
3
  const userDomains = currentDomains || [];
6
4
 
7
5
  return multiDomainEnabled ? (userDomains?.length > 0 ? userDomains : [null]) : [];