@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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ 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
+
8
+ ## [0.0.60] - 09.06.2026
9
+
10
+ - History table: theme, follow up, quality sorting
11
+ - History table: header filter positioning
12
+
13
+ ## [0.0.59] - 05.06.2026
14
+
15
+ - Quality measurements in chat history view
16
+ - Column sorting and filtering
17
+ - Analytics: theme, follow up action, response quality
18
+
7
19
  ## [0.0.58] - 25.05.2026
8
20
 
9
21
  - Properly render mcq selected button
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buerokratt-ria/common-gui-components",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
4
4
  "description": "Common GUI components and pre defined templates.",
5
5
  "main": "index.ts",
6
6
  "author": "ExiRai",
@@ -17,6 +17,14 @@
17
17
  overflow: hidden;
18
18
  }
19
19
 
20
+ .history-header-combobox {
21
+ display: inline-flex;
22
+ vertical-align: middle;
23
+ }
24
+
25
+ .data-table th:has(.history-header-combobox .select--open) {
26
+ z-index: 10001 !important;
27
+ }
20
28
 
21
29
  .card-drawer-container {
22
30
  display: flex;
@@ -90,7 +98,13 @@
90
98
  font-size: $veera-font-size-80;
91
99
  gap: 4px;
92
100
  overflow-y: auto;
93
- padding: get-spacing(haapsalu);
101
+ position: relative;
102
+
103
+ .side-meta__content {
104
+ display: grid;
105
+ gap: 4px;
106
+ padding: get-spacing(haapsalu);
107
+ }
94
108
  }
95
109
 
96
110
  .spinner {
@@ -0,0 +1,18 @@
1
+ @import 'src/styles/settings/variables/typography';
2
+
3
+ .metadata-item {
4
+ &__value {
5
+ font-size: $veera-font-size-80;
6
+ }
7
+ &__meta {
8
+ font-size: $veera-font-size-70;
9
+ color: #686B78;
10
+ }
11
+ }
12
+
13
+ .divider {
14
+ height: 1px;
15
+ background-color: #686B78;
16
+ width: 100%;
17
+ margin: 16px 0;
18
+ }
@@ -0,0 +1,206 @@
1
+ import { format } from "date-fns";
2
+ import { et } from "date-fns/locale";
3
+ import { ComponentProps, FC, PropsWithChildren, useMemo } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+ import { Chat as ChatType } from "../../../../../types/chat";
6
+
7
+ import './ChatMetadataPanel.scss';
8
+ import { CharMeasurementType } from "../../types";
9
+
10
+ type ChatMetadataPanelProps = PropsWithChildren<{
11
+ readonly chat: ChatType
12
+ readonly chatMeasurments: ComponentProps<typeof Measurements>['measurments'];
13
+ }>;
14
+
15
+ const formatMetaDate = (date: string) => format(
16
+ new Date(date),
17
+ 'd.MMMM yyyy HH:mm:ss',
18
+ {
19
+ locale: et,
20
+ }
21
+ ).toLowerCase();
22
+
23
+ const formatAuthor = (value?: string) => value ? `(${value})` : '';
24
+
25
+ const formatMeta = (date?: string, author?: string) => {
26
+ const formattedDate = date ? formatMetaDate(date) : '';
27
+ const formattedAuthor = formatAuthor(author);
28
+
29
+ return [formattedDate, formattedAuthor].filter(Boolean).join(' ');
30
+ };
31
+
32
+ const ChatMetadataPanel: FC<ChatMetadataPanelProps> = ({ chat, chatMeasurments, children }) => {
33
+ const { t } = useTranslation();
34
+
35
+ const endUserFullName = useMemo(() => {
36
+ return chat.endUserFirstName !== '' &&
37
+ chat?.endUserLastName !== ''
38
+ ? `${chat.endUserFirstName} ${chat.endUserLastName}`
39
+ : t('global.anonymous');
40
+ }, [chat, t]);
41
+
42
+ const commentMeta = useMemo(
43
+ () => formatMeta(chat.commentAddedDate, chat.commentAuthor),
44
+ [chat.commentAddedDate, chat.commentAuthor]
45
+ );
46
+
47
+ const statusMeta = useMemo(
48
+ () => formatMeta(chat.lastMessageTimestamp, chat.userDisplayName),
49
+ [chat.lastMessageTimestamp, chat.userDisplayName]
50
+ );
51
+
52
+ return <>
53
+ <div className="side-meta">
54
+ <div className="side-meta__content">
55
+ <MetadataItem label="ID" value={chat.id} />
56
+ <MetadataItem label={t('chat.endUser')} value={endUserFullName} />
57
+ {chat.endUserId && (
58
+ <MetadataItem label={t('chat.endUserId')} value={chat.endUserId ?? ''} />
59
+ )}
60
+ {chat.endUserEmail && (
61
+ <MetadataItem label={t('chat.endUserEmail')} value={chat.endUserEmail} />
62
+ )}
63
+ {chat.endUserPhone && (
64
+ <MetadataItem label={t('chat.endUserPhoneNumber')} value={chat.endUserPhone} />
65
+ )}
66
+ {chat.customerSupportDisplayName && (
67
+ <MetadataItem label={t('chat.csaName')} value={chat.customerSupportDisplayName} />
68
+ )}
69
+ <MetadataItem
70
+ label={t('chat.startedAt')}
71
+ value={format(
72
+ new Date(chat.created),
73
+ 'dd. MMMM Y HH:mm:ss',
74
+ {
75
+ locale: et,
76
+ }
77
+ ).toLowerCase()}
78
+ />
79
+ <MetadataItem label={t('chat.device')} value={chat.endUserOs ?? ''} />
80
+ <MetadataItem label={t('chat.location')} value={chat.endUserUrl ?? ''} />
81
+ {chat.comment && (
82
+ <MetadataItem
83
+ label={t('chat.history.comment')}
84
+ value={chat.comment}
85
+ meta={commentMeta}
86
+ />
87
+ )}
88
+ {chat.lastMessageEvent && (
89
+ <MetadataItem
90
+ label={t('global.status')}
91
+ value={t('chat.plainEvents.' + chat.lastMessageEvent)}
92
+ meta={statusMeta}
93
+ />
94
+ )}
95
+ <Measurements measurments={chatMeasurments} />
96
+ </div>
97
+ {children}
98
+ </div>
99
+ </>;
100
+ };
101
+
102
+ const Measurements: FC<{
103
+ readonly measurments: CharMeasurementType[];
104
+ }> = ({ measurments }) => {
105
+ const { t } = useTranslation();
106
+
107
+ const groupedMeasurements = useMemo(() => {
108
+ const typeOrder: CharMeasurementType['type'][] = [
109
+ 'THEME',
110
+ 'QUALITY',
111
+ 'FOLLOW_UP_ACTION',
112
+ ];
113
+
114
+ const grouped = measurments.reduce<
115
+ Partial<
116
+ Record<
117
+ CharMeasurementType['type'],
118
+ Record<
119
+ string,
120
+ {
121
+ createdAt: string;
122
+ authorDisplayName: string;
123
+ values: string[];
124
+ }
125
+ >
126
+ >
127
+ >
128
+ >((acc, item) => {
129
+ acc[item.type] ??= {};
130
+
131
+ acc[item.type]![item.createdAt] ??= {
132
+ createdAt: item.createdAt,
133
+ authorDisplayName: item.authorDisplayName,
134
+ values: [],
135
+ };
136
+
137
+ if (item.value) {
138
+ acc[item.type]![item.createdAt].values.push(item.value);
139
+ }
140
+
141
+ return acc;
142
+ }, {});
143
+
144
+ return typeOrder
145
+ .map((type) => ({
146
+ type,
147
+ items: Object.values(grouped[type] ?? {})
148
+ .map((item) => ({
149
+ createdAt: item.createdAt,
150
+ authorDisplayName: item.authorDisplayName,
151
+ value: item.values.join(', '),
152
+ }))
153
+ .sort(
154
+ (a, b) =>
155
+ new Date(b.createdAt).getTime() -
156
+ new Date(a.createdAt).getTime()
157
+ ),
158
+ }))
159
+ .filter(({ items }) => items.length > 0);
160
+ }, [measurments]);
161
+
162
+ const getLabel = (type: CharMeasurementType['type']) =>
163
+ ({
164
+ THEME: t('chat.quality.theme'),
165
+ QUALITY: t('chat.quality.responseQuality'),
166
+ FOLLOW_UP_ACTION: t('chat.quality.followUpAction'),
167
+ })[type];
168
+
169
+ return (
170
+ <>
171
+ <div className="divider"></div>
172
+
173
+ {groupedMeasurements.map(({ type, items }) =>
174
+ items.map((item, index) => (
175
+ <MetadataItem
176
+ labelStyle={{ marginTop: '12px' }}
177
+ key={`${type}-${item.createdAt}`}
178
+ {...((index === 0) ? { label: getLabel(type) } : {})}
179
+ value={item.value || t('chat.quality.selectionEmptied')}
180
+ meta={formatMeta(item.createdAt, item.authorDisplayName)}
181
+ />
182
+ ))
183
+ )}
184
+ </>
185
+ );
186
+ };
187
+
188
+
189
+ type MetadataItemProps = {
190
+ readonly label?: string;
191
+ readonly value: string;
192
+ readonly meta?: string;
193
+ readonly labelStyle?: React.CSSProperties;
194
+ };
195
+
196
+ const MetadataItem: FC<MetadataItemProps> = ({ label, value, meta, labelStyle }) => {
197
+ return <>
198
+ {label && <p style={labelStyle}>
199
+ <strong>{label}</strong>
200
+ </p>}
201
+ <p className="metadata-item__value">{value}</p>
202
+ {meta && <p className="metadata-item__meta">{meta}</p>}
203
+ </>;
204
+ }
205
+
206
+ export { ChatMetadataPanel };
@@ -0,0 +1,17 @@
1
+ import { FC } from "react";
2
+
3
+ type MetadataItemProps = {
4
+ readonly label: string,
5
+ readonly value: string
6
+ };
7
+
8
+ const ChatMetadataPanelItem: FC<MetadataItemProps> = ({ label, value }) => {
9
+ return <>
10
+ <p>
11
+ <strong>{label}</strong>
12
+ </p>
13
+ <p>{value}</p>
14
+ </>;
15
+ }
16
+
17
+ export { ChatMetadataPanelItem };
@@ -0,0 +1,42 @@
1
+ @import 'src/styles/settings/variables/typography';
2
+
3
+ .filter-tag {
4
+ align-items: center;
5
+ appearance: none;
6
+ background-color: #EAF6FF;
7
+ border-radius: 4px;
8
+ border: 1px solid #CCDEED;
9
+ color: #131317;
10
+ cursor: pointer;
11
+ display: flex;
12
+ font-family: inherit;
13
+ font-size: $veera-font-size-80;
14
+ line-height: $veera-font-size-100;
15
+ padding: 4px 4px 4px 8px;
16
+
17
+ &:hover {
18
+ background-color: #CCDEED;
19
+ border-color: #99BDDA;
20
+
21
+ .icon {
22
+ color: #004882;
23
+ }
24
+ }
25
+
26
+ &:focus {
27
+ background-color: #003662;
28
+ border-color: #003662;
29
+ color: #FFFFFF;
30
+
31
+ .icon {
32
+ color: #FFFFFF;
33
+ }
34
+ }
35
+
36
+
37
+ .icon {
38
+ cursor: pointer;
39
+ margin-left: 4px;
40
+ color: #686B78;
41
+ }
42
+ }
@@ -0,0 +1,16 @@
1
+ import { FC } from "react"
2
+ import { MdClose } from "react-icons/md";
3
+ import './FilterTag.scss';
4
+
5
+ type FilterTagProps = {
6
+ readonly text: string;
7
+ readonly onClick: () => void;
8
+ }
9
+
10
+ export const FilterTag: FC<FilterTagProps> = ({ text, onClick }) => {
11
+ return <>
12
+ <button className="filter-tag" type="button" onClick={onClick}>
13
+ {text} <MdClose size="16" className="icon" />
14
+ </button>
15
+ </>
16
+ }
@@ -0,0 +1,67 @@
1
+ import { ComponentProps, FC } from 'react';
2
+
3
+ import { FormCombobox } from '../../../../../ui-components';
4
+
5
+ type HeaderComboboxBaseProps = {
6
+ readonly label: string;
7
+ readonly options?: ComponentProps<typeof FormCombobox>['options'];
8
+ readonly isSearchEnabled?: ComponentProps<typeof FormCombobox>['isSearchEnabled'];
9
+ };
10
+
11
+ type HeaderComboboxSingleProps = HeaderComboboxBaseProps & {
12
+ readonly multiple: false;
13
+ readonly value?: string;
14
+ readonly onChange: (value: string) => void;
15
+ };
16
+
17
+ type HeaderComboboxMultipleProps = HeaderComboboxBaseProps & {
18
+ readonly multiple?: true;
19
+ readonly value?: string[];
20
+ readonly onChange: (value: string[]) => void;
21
+ };
22
+
23
+ type HeaderComboboxProps = HeaderComboboxSingleProps | HeaderComboboxMultipleProps;
24
+
25
+ const HeaderCombobox: FC<HeaderComboboxProps> = (props) => {
26
+ const {
27
+ label,
28
+ options = [],
29
+ isSearchEnabled = true,
30
+ } = props;
31
+ const sharedProps = {
32
+ hideInputStyle: true,
33
+ hideLabel: true,
34
+ label,
35
+ placeholder: label,
36
+ searchPlaceholder: label,
37
+ options,
38
+ isSearchEnabled,
39
+ isMenuPortaled: true,
40
+ };
41
+
42
+ return (
43
+ <span
44
+ className="history-header-combobox"
45
+ onMouseDown={(event) => event.stopPropagation()}
46
+ onClick={(event) => event.stopPropagation()}
47
+ >
48
+ {props.multiple === false ? (
49
+ <FormCombobox
50
+ {...sharedProps}
51
+ multiple={false}
52
+ value={props.value}
53
+ onChange={props.onChange}
54
+ />
55
+ ) : (
56
+ <FormCombobox
57
+ {...sharedProps}
58
+ multiple={true}
59
+ value={props.value}
60
+ onChange={props.onChange}
61
+ />
62
+ )}
63
+ </span>
64
+ );
65
+ };
66
+
67
+ export { HeaderCombobox };
@@ -0,0 +1,19 @@
1
+ @import 'src/styles/tools/spacing';
2
+
3
+ .title {
4
+ font-size: 16px;
5
+ font-weight: 400;
6
+ line-height: 20px;
7
+ color: #131317;
8
+ }
9
+
10
+ .quality-settings {
11
+ background-color: #ffffff;
12
+ border-top: 1px solid #e0e0e0;
13
+ bottom: 0;
14
+ gap: 8px;
15
+ margin-top: auto;
16
+ padding: get-spacing(haapsalu);
17
+ position: sticky;
18
+ width: 100%;
19
+ }
@@ -0,0 +1,115 @@
1
+ import { ComponentProps, FC } from 'react';
2
+ import { Controller, useForm } from 'react-hook-form';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ import { FormCombobox, Track } from '../../../../../ui-components';
6
+
7
+ import './QualitySettings.scss';
8
+
9
+ type QualitySettingsProps = {
10
+ readonly theme: {
11
+ readonly onChange: (value: string[]) => void;
12
+ readonly options: Pick<ComponentProps<typeof FormCombobox>, 'options'>['options'];
13
+ readonly value: string[];
14
+ };
15
+ readonly quality: {
16
+ readonly onChange: (value: string) => void;
17
+ readonly options: Pick<ComponentProps<typeof FormCombobox>, 'options'>['options'];
18
+ readonly value?: string;
19
+ };
20
+ readonly followUp: {
21
+ readonly onChange: (value: string) => void;
22
+ readonly options: Pick<ComponentProps<typeof FormCombobox>, 'options'>['options'];
23
+ readonly value?: string;
24
+ };
25
+ };
26
+
27
+ const QualitySettings: FC<QualitySettingsProps> = ({ theme, quality, followUp }) => {
28
+ const { t } = useTranslation();
29
+ const form = useForm<{
30
+ readonly theme: string[];
31
+ readonly quality: string;
32
+ readonly followUp: string;
33
+ }>({
34
+ defaultValues: {
35
+ theme: [],
36
+ quality: '',
37
+ followUp: '',
38
+ },
39
+ values: {
40
+ theme: theme.value,
41
+ quality: quality.value ?? '',
42
+ followUp: followUp.value ?? '',
43
+ },
44
+ });
45
+
46
+ const handleThemeChange = (value: string[], onChange: (value: string[]) => void) => {
47
+ onChange(value);
48
+ theme.onChange(value);
49
+ };
50
+
51
+ const handleQualityChange = (value: string, onChange: (value: string) => void) => {
52
+ onChange(value);
53
+ quality.onChange(value);
54
+ };
55
+
56
+ const handleFollowUpChange = (value: string, onChange: (value: string) => void) => {
57
+ onChange(value);
58
+ followUp.onChange(value);
59
+ };
60
+
61
+ return (
62
+ <Track
63
+ gap={8}
64
+ direction="vertical"
65
+ align="left"
66
+ className="quality-settings"
67
+ >
68
+ <p className="title">{t('chat.history.analysis')}</p>
69
+
70
+ <Controller
71
+ name="theme"
72
+ control={form.control}
73
+ render={({ field }) => (
74
+ <FormCombobox
75
+ multiple={true}
76
+ placeholder={t('chat.history.chooseConversationTheme')}
77
+ searchPlaceholder={t('chat.history.conversationTheme')}
78
+ options={theme.options}
79
+ value={field.value}
80
+ onChange={(value) => handleThemeChange(value, field.onChange)}
81
+ isSearchEnabled={true}
82
+ />
83
+ )}
84
+ />
85
+ <Controller
86
+ name="quality"
87
+ control={form.control}
88
+ render={({ field }) => (
89
+ <FormCombobox
90
+ placeholder={t('chat.history.chooseConversationResponseQuality')}
91
+ searchPlaceholder={t('chat.history.conversationResponseQuality')}
92
+ options={quality.options}
93
+ value={field.value}
94
+ onChange={(value) => handleQualityChange(value, field.onChange)}
95
+ />
96
+ )}
97
+ />
98
+ <Controller
99
+ name="followUp"
100
+ control={form.control}
101
+ render={({ field }) => (
102
+ <FormCombobox
103
+ placeholder={t('chat.history.chooseFollowUpAction')}
104
+ searchPlaceholder={t('chat.history.followUpAction')}
105
+ options={followUp.options}
106
+ value={field.value}
107
+ onChange={(value) => handleFollowUpChange(value, field.onChange)}
108
+ />
109
+ )}
110
+ />
111
+ </Track>
112
+ );
113
+ };
114
+
115
+ export { QualitySettings };
@@ -0,0 +1,36 @@
1
+ @import 'src/styles/settings/variables/typography';
2
+
3
+ .selected-filters__container {
4
+ display: grid;
5
+ grid-template-columns: 1fr;
6
+ grid-template-columns: 24px 1fr auto;
7
+ margin-bottom: 16px;
8
+
9
+ .selected-filters {
10
+ display: flex;
11
+ flex-wrap: wrap;
12
+ gap: 4px;
13
+
14
+ .selected-filters__label {
15
+ color: #686B78;
16
+ font-size: $veera-font-size-80;
17
+ line-height: $veera-font-size-250;
18
+ // max-height -> border-left is with correct height!
19
+ max-height: 24px;
20
+ padding-left: 4px;
21
+ white-space: nowrap;
22
+
23
+ &:not(:first-child):not(:last-child) {
24
+ border-left: 1px solid #878A97;
25
+ }
26
+ }
27
+
28
+ .selected-filters__items {
29
+ display: contents;
30
+ }
31
+ }
32
+
33
+ button {
34
+ align-self: start;
35
+ }
36
+ }