@vtex/faststore-plugin-buyer-portal 1.3.16 → 1.3.18

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 (34) hide show
  1. package/.github/workflows/betarelease.yaml +69 -0
  2. package/CHANGELOG.md +24 -6
  3. package/beta-release.sh +23 -0
  4. package/package.json +3 -2
  5. package/src/features/budgets/components/BudgetDeleteDrawer/BudgetDeleteDrawer.tsx +1 -1
  6. package/src/features/budgets/components/BudgetEditNotificationDrawer/BudgetEditNotificationDrawer.tsx +139 -0
  7. package/src/features/budgets/components/BudgetEditNotificationDrawer/budget-edit-notification-drawer.scss +34 -0
  8. package/src/features/budgets/components/BudgetNotificationForm/BudgetNotificationForm.tsx +361 -0
  9. package/src/features/budgets/components/BudgetNotificationForm/budget-notification-form.scss +219 -0
  10. package/src/features/budgets/components/BudgetNotificationsInfo/BudgetNotificationsInfo.tsx +116 -0
  11. package/src/features/budgets/components/BudgetNotificationsInfo/budget-notifications-info.scss +97 -0
  12. package/src/features/budgets/components/BudgetUsersTable/BudgetUsersTable.tsx +118 -0
  13. package/src/features/budgets/components/BudgetUsersTable/budget-users-table.scss +65 -0
  14. package/src/features/budgets/components/BudgetsTable/BudgetsTable.tsx +10 -0
  15. package/src/features/budgets/components/CreateBudgetAllocationDrawer/CreateBudgetAllocationDrawer.tsx +1 -1
  16. package/src/features/budgets/components/CreateBudgetDrawer/CreateBudgetDrawer.tsx +86 -25
  17. package/src/features/budgets/components/CreateBudgetDrawer/create-budget-drawer.scss +6 -0
  18. package/src/features/budgets/components/DeleteBudgetAllocationDrawer/DeleteBudgetAllocationDrawer.tsx +1 -1
  19. package/src/features/budgets/components/EditBudgetDrawer/EditBudgetDrawer.tsx +40 -1
  20. package/src/features/budgets/components/EditBudgetDrawer/edit-budget-drawer.scss +5 -0
  21. package/src/features/budgets/hooks/useDebouncedSearchBudgetNotification.ts +37 -0
  22. package/src/features/budgets/hooks/useListUsers.ts +1 -1
  23. package/src/features/budgets/layouts/BudgetsDetailsLayout/BudgetsDetailsLayout.tsx +9 -1
  24. package/src/features/budgets/layouts/BudgetsDetailsLayout/budget-details-layout.scss +14 -1
  25. package/src/features/budgets/layouts/BudgetsLayout/BudgetsLayout.tsx +39 -0
  26. package/src/features/budgets/layouts/BudgetsLayout/budgets-layout.scss +1 -1
  27. package/src/features/budgets/types/index.ts +17 -0
  28. package/src/features/shared/components/AutocompleteDropdown/AutocompleteDropdownItem.tsx +4 -0
  29. package/src/features/shared/components/QuantitySelectorWithPercentage/QuantitySelectorWithPercentage.tsx +150 -0
  30. package/src/features/shared/components/index.ts +24 -23
  31. package/src/features/shared/types/CurrencyType.d.ts +4 -0
  32. package/src/features/shared/types/index.ts +4 -3
  33. package/src/features/shared/utils/budgetAmountParse.ts +24 -0
  34. package/src/features/shared/utils/constants.ts +1 -1
@@ -0,0 +1,361 @@
1
+ import { ChangeEvent, useEffect, useMemo, useState } from "react";
2
+
3
+ import { Icon, Toggle } from "@faststore/ui";
4
+
5
+ import {
6
+ AutocompleteDropdown,
7
+ QuantitySelectorWithPercentage,
8
+ Table,
9
+ } from "../../../shared/components";
10
+ import { CurrencyType, LocaleType } from "../../../shared/types";
11
+ import { useDebouncedSearchBudgetNotification } from "../../hooks/useDebouncedSearchBudgetNotification";
12
+ import BudgetUsersTable from "../BudgetUsersTable/BudgetUsersTable";
13
+
14
+ import type {
15
+ BudgetInput,
16
+ BudgetNotification,
17
+ NotificationThresholds,
18
+ NotificationUsers,
19
+ } from "../../types";
20
+
21
+ type BudgetNotificationFormProps = {
22
+ budget: BudgetInput;
23
+ contractId: string;
24
+ unitId: string;
25
+ handleChange: <K extends keyof BudgetInput>(
26
+ field: K,
27
+ value: BudgetInput[K]
28
+ ) => void;
29
+ readonly?: boolean;
30
+ totalAmount?: number;
31
+ onThresholdsChange?: (thresholds: number[]) => void;
32
+ currency?: CurrencyType;
33
+ locale?: LocaleType;
34
+ showUsersError?: boolean;
35
+ };
36
+
37
+ type UserOption = { id: string; name: string; email?: string };
38
+
39
+ export const BudgetNotificationForm = ({
40
+ budget,
41
+ contractId,
42
+ unitId,
43
+ handleChange,
44
+ readonly = false,
45
+ totalAmount,
46
+ currency = "USD",
47
+ locale = "en-US",
48
+ showUsersError = false,
49
+ }: BudgetNotificationFormProps) => {
50
+ const [enabled, setEnabled] = useState<boolean>(
51
+ Boolean(budget?.notifications?.hasNotification)
52
+ );
53
+
54
+ const [thresholds, setThresholds] = useState<NotificationThresholds[]>(
55
+ budget?.notifications?.thresholds?.length
56
+ ? budget.notifications.thresholds
57
+ : [{ value: 50 }]
58
+ );
59
+
60
+ const [inputValue, setInputValue] = useState<string>("");
61
+ const [options, setOptions] = useState<UserOption[]>([]);
62
+ const [selectedUsers, setSelectedUsers] = useState<NotificationUsers[]>(
63
+ budget?.notifications?.users?.length ? budget.notifications.users : []
64
+ );
65
+
66
+ const parsedTotal = useMemo(() => {
67
+ if (typeof totalAmount === "number") return totalAmount;
68
+ const raw =
69
+ typeof budget?.amount === "string"
70
+ ? budget.amount.replace(/[^\d.,-]/g, "").replace(",", ".")
71
+ : budget?.amount;
72
+ const n = Number(raw);
73
+ return Number.isFinite(n) ? n : 0;
74
+ }, [totalAmount, budget?.amount]);
75
+
76
+ const fmtCurrency = (value: number) =>
77
+ new Intl.NumberFormat(locale, { style: "currency", currency }).format(
78
+ value
79
+ );
80
+
81
+ useEffect(() => {
82
+ if (enabled && thresholds.length === 0) {
83
+ setThresholds([{ value: 50 }]);
84
+ }
85
+ }, [enabled, thresholds.length]);
86
+
87
+ useEffect(() => {
88
+ setEnabled(Boolean(budget?.notifications?.hasNotification));
89
+ }, [budget?.notifications?.hasNotification]);
90
+
91
+ useEffect(() => {
92
+ const prev = budget.notifications;
93
+
94
+ const next: BudgetNotification = {
95
+ hasNotification: enabled,
96
+ thresholds: enabled ? thresholds : [],
97
+ users: selectedUsers,
98
+ };
99
+
100
+ const sameHas = prev?.hasNotification === next.hasNotification;
101
+
102
+ const sameThresholds =
103
+ Array.isArray(prev?.thresholds) &&
104
+ prev!.thresholds.length === next.thresholds.length &&
105
+ prev!.thresholds.every((t, i) => t.value === next.thresholds[i].value);
106
+
107
+ const sameUsers =
108
+ Array.isArray(prev?.users) &&
109
+ prev!.users.length === next.users.length &&
110
+ prev!.users.every(
111
+ (u, i) =>
112
+ u.userId === next.users[i].userId &&
113
+ u.userName === next.users[i].userName &&
114
+ u.userEmail === next.users[i].userEmail
115
+ );
116
+
117
+ const isSame = sameHas && sameThresholds && sameUsers;
118
+
119
+ if (!isSame) {
120
+ handleChange("notifications", next);
121
+ }
122
+ }, [enabled, thresholds, selectedUsers, handleChange, budget.notifications]);
123
+
124
+ const addThreshold = () => {
125
+ if (readonly || parsedTotal <= 0) return;
126
+ setThresholds((prev) =>
127
+ prev.length >= 5 ? prev : [...prev, { value: 50 }]
128
+ );
129
+ };
130
+
131
+ const removeThreshold = (idx: number) => {
132
+ if (readonly) return;
133
+ setThresholds((prev) => prev.filter((_, i) => i !== idx));
134
+ };
135
+
136
+ const onToggleNotifications = (checked: boolean) => {
137
+ setEnabled(checked);
138
+ if (checked && thresholds.length === 0) {
139
+ setThresholds([{ value: 50 }]);
140
+ }
141
+ };
142
+
143
+ const { usersResponse, isLoadingUsers, isDebouncing } =
144
+ useDebouncedSearchBudgetNotification({
145
+ customerId: contractId,
146
+ unitId,
147
+ search: inputValue,
148
+ });
149
+ const loadingObject: UserOption = { id: "loading", name: "Loading..." };
150
+
151
+ useEffect(() => {
152
+ const mapped: UserOption[] = (usersResponse?.users ?? []).map((u) => ({
153
+ id: String(u.userId),
154
+ name: String(u.name ?? "Usuário"),
155
+ email: u.email,
156
+ }));
157
+ setOptions(mapped);
158
+ }, [usersResponse]);
159
+
160
+ const addUserIfNotExists = (opt: UserOption) => {
161
+ const exists = selectedUsers.some((u) => u.userId === opt.id);
162
+ if (readonly || exists) return;
163
+
164
+ const next: NotificationUsers[] = [
165
+ ...selectedUsers,
166
+ {
167
+ userId: opt.id,
168
+ userName: opt.name,
169
+ userEmail: opt.email ?? "",
170
+ },
171
+ ];
172
+ setSelectedUsers(next);
173
+ setInputValue("");
174
+ };
175
+
176
+ const handleRemove = (id: string) => {
177
+ const next = selectedUsers.filter((u) => u.userId !== id);
178
+ setSelectedUsers(next);
179
+ };
180
+
181
+ const sizeProps = { width: 20, height: 20 };
182
+ const disableRows = readonly || !enabled;
183
+
184
+ return (
185
+ <div data-fs-bp-budget-notification>
186
+ <div
187
+ data-fs-bp-budget-notification-row-section
188
+ data-fs-bp-budget-notification-renew
189
+ >
190
+ <span data-fs-bp-budget-notification-renew-label>Notifications</span>
191
+ <div data-fs-bp-budget-notification-row-section-action>
192
+ <Toggle
193
+ id="notificationsToggle"
194
+ checked={enabled}
195
+ onChange={(e) => onToggleNotifications(e.target.checked)}
196
+ disabled={readonly}
197
+ />
198
+ </div>
199
+ </div>
200
+
201
+ <div data-fs-bp-budget-notification-section-label>
202
+ <span>
203
+ Set up to 5 thresholds and notify users when the total amount reaches
204
+ specific percentages
205
+ </span>
206
+ </div>
207
+
208
+ {enabled && (
209
+ <>
210
+ <div>
211
+ <div data-fs-bp-budget-notification-table>
212
+ <Table layoutFixed>
213
+ <Table.Head
214
+ columns={[
215
+ { key: "threshold", label: "Threshold", size: "medium" },
216
+ {
217
+ key: "consumed",
218
+ label: "Consumed",
219
+ size: "medium",
220
+ align: "right",
221
+ },
222
+ {
223
+ key: "remaining",
224
+ label: "Remaining",
225
+ size: "large",
226
+ align: "right",
227
+ },
228
+ { key: "actions", label: "", size: "3rem" },
229
+ ]}
230
+ />
231
+
232
+ <Table.Body>
233
+ {thresholds.map((t, idx) => {
234
+ const consumed = (parsedTotal * t.value) / 100;
235
+ const remaining = Math.max(parsedTotal - consumed, 0);
236
+
237
+ return (
238
+ <Table.Row
239
+ key={`${idx}-${t.value}`}
240
+ title={
241
+ <div data-fs-bp-budget-notification-stepper>
242
+ <QuantitySelectorWithPercentage
243
+ min={1}
244
+ max={100}
245
+ initial={t.value}
246
+ formatAsPercent
247
+ disabled={disableRows || parsedTotal <= 0}
248
+ onChange={(val: number) =>
249
+ setThresholds((prev) =>
250
+ prev.map((th, i) =>
251
+ i === idx
252
+ ? {
253
+ value: Math.min(
254
+ 100,
255
+ Math.max(1, val)
256
+ ),
257
+ }
258
+ : th
259
+ )
260
+ )
261
+ }
262
+ />
263
+ </div>
264
+ }
265
+ enabled={!disableRows}
266
+ actionIcons={
267
+ <Icon
268
+ name="MinusCircle"
269
+ {...sizeProps}
270
+ data-fs-bp-budget-notification-remove
271
+ onClick={() => removeThreshold(idx)}
272
+ aria-label="Remove threshold"
273
+ />
274
+ }
275
+ >
276
+ <Table.Cell>
277
+ <span data-fs-bp-budget-notification-money>
278
+ {fmtCurrency(consumed)}
279
+ </span>
280
+ </Table.Cell>
281
+ <Table.Cell>
282
+ <span data-fs-bp-budget-notification-money>
283
+ {fmtCurrency(remaining)}
284
+ </span>
285
+ </Table.Cell>
286
+ </Table.Row>
287
+ );
288
+ })}
289
+ </Table.Body>
290
+ </Table>
291
+ </div>
292
+
293
+ <div data-fs-bp-budget-notification-add>
294
+ <button
295
+ type="button"
296
+ onClick={addThreshold}
297
+ disabled={disableRows || thresholds.length >= 5}
298
+ >
299
+ Add threshold
300
+ </button>
301
+ </div>
302
+ </div>
303
+
304
+ <div data-fs-bp-budget-notification-section-user>
305
+ <div data-fs-bp-budget-notification-section-label>
306
+ <span>
307
+ Add users to be notified by email. At least one user must be
308
+ added
309
+ </span>
310
+ </div>
311
+
312
+ <AutocompleteDropdown
313
+ label="Add users"
314
+ value={inputValue}
315
+ shouldOpenOnFocus={false}
316
+ autoComplete="off"
317
+ shouldShowArrowDown={false}
318
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
319
+ setInputValue(e.target.value);
320
+ }}
321
+ options={
322
+ isLoadingUsers || isDebouncing ? [loadingObject] : options
323
+ }
324
+ hasError={showUsersError && enabled && selectedUsers.length === 0}
325
+ helperLabel={
326
+ showUsersError && enabled && selectedUsers.length === 0
327
+ ? "Select at least one user."
328
+ : undefined
329
+ }
330
+ renderOption={(option, index) => (
331
+ <AutocompleteDropdown.Item
332
+ key={option.id}
333
+ closeOnClick
334
+ index={index}
335
+ isSelected={false}
336
+ onClick={() => addUserIfNotExists(option)}
337
+ >
338
+ <div data-fs-bp-budget-notification-autocomplete>
339
+ <span>{option.name}</span>
340
+ <span>{option.email ? `${option.email}` : ""}</span>
341
+ </div>
342
+ </AutocompleteDropdown.Item>
343
+ )}
344
+ />
345
+
346
+ {selectedUsers.length > 0 && (
347
+ <BudgetUsersTable
348
+ users={selectedUsers.map((u) => ({
349
+ userId: u.userId,
350
+ userName: u.userName,
351
+ email: u.userEmail || "",
352
+ }))}
353
+ onRemove={(id: string) => handleRemove(id)}
354
+ />
355
+ )}
356
+ </div>
357
+ </>
358
+ )}
359
+ </div>
360
+ );
361
+ };
@@ -0,0 +1,219 @@
1
+ [data-fs-bp-budget-notification] {
2
+ @import "@faststore/ui/src/components/molecules/Toggle/styles.scss";
3
+ @import "@faststore/ui/src/components/molecules/Tooltip/styles.scss";
4
+ @import "@faststore/ui/src/components/molecules/QuantitySelector/styles.scss";
5
+ @import "../../../shared/components/InputText/input-text.scss";
6
+ @import "../../../shared/components/Table/table.scss";
7
+ @import "../../../shared/components/ErrorMessage/error-message.scss";
8
+ @import "../BudgetUsersTable/budget-users-table.scss";
9
+
10
+ padding-bottom: var(--fs-spacing-7);
11
+
12
+ [data-fs-bp-budget-notification-section-user] {
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: var(--fs-spacing-3);
16
+ margin-top: var(--fs-spacing-5);
17
+
18
+ [data-fs-bp-budget-notification-autocomplete] {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: var(--fs-spacing-1);
22
+ span:first-child {
23
+ font-weight: var(--fs-text-weight-semibold);
24
+ color: #000000;
25
+ }
26
+ }
27
+ }
28
+
29
+ [data-fs-bp-budget-notification-section-label] {
30
+ font-weight: var(--fs-text-weight-regular);
31
+ font-size: var(--fs-text-size-1);
32
+ color: #5c5c5c;
33
+ line-height: calc(var(--fs-spacing-3) + var(--fs-spacing-0));
34
+ }
35
+
36
+ [data-fs-bp-budget-notification-row-section] {
37
+ display: flex;
38
+ flex-direction: column;
39
+ width: 100%;
40
+
41
+ @include media(">=notebook") {
42
+ align-items: center;
43
+ flex-direction: row;
44
+ gap: var(--fs-spacing-3);
45
+ }
46
+
47
+ &[data-fs-bp-budget-notification-renew] {
48
+ justify-content: space-between;
49
+ flex-direction: row;
50
+
51
+ [data-fs-bp-budget-notification-renew-label] {
52
+ font-weight: var(--fs-text-weight-semibold);
53
+ font-size: var(--fs-text-size-2);
54
+ color: #000000;
55
+ margin-bottom: var(--fs-spacing-1);
56
+ }
57
+ [data-fs-toggle] {
58
+ width: var(--fs-spacing-6);
59
+ height: var(--fs-spacing-4);
60
+ min-width: auto;
61
+ }
62
+ }
63
+ }
64
+
65
+ [data-fs-bp-autocomplete-dropdown]{
66
+ [data-fs-bp-autocomplete-dropdown-menu]{
67
+ z-index:2;
68
+ }
69
+ }
70
+
71
+ [data-fs-bp-budget-notification-data-section] {
72
+ & > div {
73
+ flex: 1;
74
+ }
75
+ }
76
+
77
+ [data-fs-bp-basic-drawer-footer] {
78
+ justify-content: flex-end;
79
+ gap: var(--fs-spacing-1);
80
+ }
81
+ [data-fs-bp-budget-notification-row-section-action] {
82
+ display: flex;
83
+ flex-direction: row;
84
+ gap: var(--fs-spacing-3);
85
+ align-items: center;
86
+ }
87
+
88
+ [data-fs-bp-budget-notification-table] {
89
+ margin-top: var(--fs-spacing-3);
90
+ width: 100%;
91
+ display: block;
92
+
93
+ [data-fs-bp-table] {
94
+ [data-fs-bp-table-head] {
95
+ [data-fs-bp-table-head-row] {
96
+ border-bottom: var(--fs-border-radius-small) solid #e0e0e0;
97
+
98
+ th[data-fs-bp-table-head-column] {
99
+ text-align: start;
100
+ font-weight: var(--fs-text-weight-medium);
101
+ font-size: var(--fs-text-size-1);
102
+ color: #5c5c5c;
103
+ padding: var(--fs-spacing-2) 0;
104
+ }
105
+
106
+ th[data-fs-bp-table-head-column]:last-child {
107
+ min-width: var(--fs-spacing-4);
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ [data-fs-bp-budget-notification-stepper] {
114
+ display: inline-flex;
115
+ align-items: center;
116
+ gap: var(--fs-spacing-1);
117
+
118
+ &[data-fs-bp-budget-notification-stepper-qty-wrapper] {
119
+ position: relative;
120
+ display: inline-block;
121
+
122
+ [data-fs-bp-budget-notification-stepper-qty-suffix] {
123
+ position: absolute;
124
+ right: var(--fs-spacing-2);
125
+ top: 50%;
126
+ transform: translateY(-50%);
127
+ pointer-events: none;
128
+ opacity: 0.7;
129
+ font: inherit;
130
+ }
131
+ }
132
+
133
+ div[data-fs-quantity-selector] {
134
+ height: 2.25rem;
135
+ width: 7.75rem;
136
+ border-radius: var(--fs-border-radius-pill);
137
+ border: var(--fs-border-radius-small) solid #d6d6d6;
138
+ --fs-qty-selector-border-radius: var(--fs-border-radius-pill);
139
+
140
+ [data-fs-input] {
141
+ height: 2.25rem;
142
+ color: #000000;
143
+ font-weight: var(--fs-text-weight-regular);
144
+ font-size: var(--fs-text-size-1);
145
+ }
146
+
147
+ button {
148
+ cursor: pointer;
149
+ &:first-child {
150
+ left: 0.3125rem;
151
+ }
152
+ &:last-child {
153
+ right: 0.3125rem;
154
+ }
155
+ border: none;
156
+ [data-fs-button-wrapper] {
157
+ [data-fs-button-icon] {
158
+ svg {
159
+ color: #0068d7;
160
+ width: 1.25rem;
161
+ height: 1.25rem;
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ div[data-fs-quantity-selector="disabled"] {
169
+ [data-fs-input] {
170
+ color: #c3c3c3;
171
+ }
172
+ --fs-qty-selector-bkg-color: #ebeaea;
173
+ button {
174
+ [data-fs-button-wrapper] {
175
+ [data-fs-button-icon] {
176
+ svg {
177
+ color: #c3c3c3;
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ [data-fs-bp-budget-notification-money] {
186
+ font-weight: var(--fs-text-weight-regular);
187
+ font-size: var(--fs-text-size-1);
188
+ color: #5C5C5C;
189
+ }
190
+
191
+ [data-fs-bp-budget-notification-remove] {
192
+ color: #000000;
193
+ stroke-width: 1.3rem;
194
+ &[disabled] {
195
+ color: #9ca3af; /* gray when disabled */
196
+ cursor: not-allowed;
197
+ }
198
+ }
199
+
200
+ [data-fs-bp-budget-notification-add] {
201
+ margin-top: var(--fs-spacing-3);
202
+ display: flex;
203
+ align-items: center;
204
+ gap: var(--fs-spacing-2);
205
+
206
+ button {
207
+ height: var(--fs-spacing-6);
208
+ width: 8.4375rem;
209
+ border-radius: var(--fs-border-radius-pill);
210
+ border: var(--fs-border-radius-small) solid #d6d6d6;
211
+
212
+ background: #fff;
213
+ padding: 0 var(--fs-spacing-3);
214
+ color: #0068d7;
215
+ font-size: var(--fs-text-size-1);
216
+ font-weight: var(--fs-text-weight-semibold);
217
+ }
218
+ }
219
+ }
@@ -0,0 +1,116 @@
1
+ import { Badge } from "@faststore/ui";
2
+
3
+ import { EmptyState } from "../../../shared/components";
4
+ import { useDrawerProps } from "../../../shared/hooks";
5
+ import { BudgetEditNotificationDrawer } from "../BudgetEditNotificationDrawer/BudgetEditNotificationDrawer";
6
+
7
+ import type {
8
+ Budget,
9
+ BudgetNotification,
10
+ NotificationThresholds,
11
+ NotificationUsers,
12
+ } from "../../types";
13
+
14
+ type BudgetNotificationsInfoProps = {
15
+ initialBudget: Budget;
16
+ contractId: string;
17
+ orgUnitId: string;
18
+ budgetId: string;
19
+ };
20
+
21
+ const DetailRow = ({
22
+ label,
23
+ notifications,
24
+ }: {
25
+ label: string;
26
+ notifications: BudgetNotification;
27
+ }) => {
28
+ const isUserLabel = label === "Users";
29
+ const list = isUserLabel
30
+ ? (notifications.users as NotificationUsers[])
31
+ : (notifications.thresholds as NotificationThresholds[]);
32
+
33
+ return (
34
+ <>
35
+ <div data-fs-bp-budget-notifications-details-row>
36
+ <span data-fs-bp-budget-notifications-details-row-label>{label}</span>
37
+ <span data-fs-bp-budget-notifications-details-row-value>
38
+ {list.map((item, index) => (
39
+ <Badge key={index} size="big">
40
+ {isUserLabel
41
+ ? (item as NotificationUsers).userName
42
+ : `${(item as NotificationThresholds).value}%`}
43
+ </Badge>
44
+ ))}
45
+ </span>
46
+ </div>
47
+ <hr data-fs-bp-budget-notifications-divider />
48
+ </>
49
+ );
50
+ };
51
+
52
+ export const BudgetNotificationsInfo = ({
53
+ initialBudget,
54
+ budgetId,
55
+ orgUnitId,
56
+ contractId,
57
+ }: BudgetNotificationsInfoProps) => {
58
+ const {
59
+ open: openNotificationsDrawerProps,
60
+ isOpen: isEditDrawerOpen,
61
+ ...editDrawerProps
62
+ } = useDrawerProps();
63
+
64
+ return (
65
+ <>
66
+ <div data-fs-bp-budget-notifications>
67
+ <div data-fs-bp-budget-notifications-details>
68
+ <div data-fs-budget-notifications-edit-line>
69
+ <span data-fs-budget-notifications-edit-title>Notifications</span>
70
+ <button
71
+ type="button"
72
+ data-fs-budget-notifications-edit-button
73
+ onClick={openNotificationsDrawerProps}
74
+ >
75
+ Edit
76
+ </button>
77
+ </div>
78
+
79
+ {initialBudget.notifications?.hasNotification ? (
80
+ <>
81
+ <hr data-fs-bp-budget-notifications-divider />
82
+ <DetailRow
83
+ key="Thresholds"
84
+ label="Thresholds"
85
+ notifications={initialBudget.notifications}
86
+ />
87
+ <DetailRow
88
+ key="Users"
89
+ label="Users"
90
+ notifications={initialBudget.notifications}
91
+ />
92
+ </>
93
+ ) : (
94
+ <div data-fs-bp-budget-notifications-empty>
95
+ <EmptyState
96
+ title="No notifications"
97
+ description="Set up to 5 thresholds and notify users when the total amount reaches specific percentages."
98
+ />
99
+ </div>
100
+ )}
101
+ </div>
102
+ </div>
103
+
104
+ {isEditDrawerOpen && (
105
+ <BudgetEditNotificationDrawer
106
+ {...editDrawerProps}
107
+ isOpen={isEditDrawerOpen}
108
+ budget={initialBudget}
109
+ contractId={contractId}
110
+ orgUnitId={orgUnitId}
111
+ budgetId={budgetId}
112
+ />
113
+ )}
114
+ </>
115
+ );
116
+ };