@vtex/faststore-plugin-buyer-portal 1.3.17 → 1.3.19

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/CHANGELOG.md +24 -6
  2. package/package.json +1 -1
  3. package/src/features/budgets/components/BudgetDeleteDrawer/BudgetDeleteDrawer.tsx +1 -1
  4. package/src/features/budgets/components/BudgetEditNotificationDrawer/BudgetEditNotificationDrawer.tsx +139 -0
  5. package/src/features/budgets/components/BudgetEditNotificationDrawer/budget-edit-notification-drawer.scss +34 -0
  6. package/src/features/budgets/components/BudgetNotificationForm/BudgetNotificationForm.tsx +361 -0
  7. package/src/features/budgets/components/BudgetNotificationForm/budget-notification-form.scss +219 -0
  8. package/src/features/budgets/components/BudgetNotificationsInfo/BudgetNotificationsInfo.tsx +116 -0
  9. package/src/features/budgets/components/BudgetNotificationsInfo/budget-notifications-info.scss +97 -0
  10. package/src/features/budgets/components/BudgetUsersTable/BudgetUsersTable.tsx +118 -0
  11. package/src/features/budgets/components/BudgetUsersTable/budget-users-table.scss +65 -0
  12. package/src/features/budgets/components/BudgetsTable/BudgetsTable.tsx +10 -0
  13. package/src/features/budgets/components/CreateBudgetAllocationDrawer/CreateBudgetAllocationDrawer.tsx +1 -1
  14. package/src/features/budgets/components/CreateBudgetDrawer/CreateBudgetDrawer.tsx +86 -25
  15. package/src/features/budgets/components/CreateBudgetDrawer/create-budget-drawer.scss +6 -0
  16. package/src/features/budgets/components/DeleteBudgetAllocationDrawer/DeleteBudgetAllocationDrawer.tsx +1 -1
  17. package/src/features/budgets/components/EditBudgetDrawer/EditBudgetDrawer.tsx +40 -1
  18. package/src/features/budgets/components/EditBudgetDrawer/edit-budget-drawer.scss +5 -0
  19. package/src/features/budgets/hooks/useDebouncedSearchBudgetNotification.ts +37 -0
  20. package/src/features/budgets/hooks/useListUsers.ts +1 -1
  21. package/src/features/budgets/layouts/BudgetsDetailsLayout/BudgetsDetailsLayout.tsx +9 -1
  22. package/src/features/budgets/layouts/BudgetsDetailsLayout/budget-details-layout.scss +14 -1
  23. package/src/features/budgets/layouts/BudgetsLayout/BudgetsLayout.tsx +39 -0
  24. package/src/features/budgets/layouts/BudgetsLayout/budgets-layout.scss +1 -1
  25. package/src/features/budgets/types/index.ts +17 -0
  26. package/src/features/shared/components/AutocompleteDropdown/AutocompleteDropdownItem.tsx +4 -0
  27. package/src/features/shared/components/OrgUnitInputSearch/OrgUnitInputSearch.tsx +17 -13
  28. package/src/features/shared/components/QuantitySelectorWithPercentage/QuantitySelectorWithPercentage.tsx +150 -0
  29. package/src/features/shared/components/index.ts +24 -23
  30. package/src/features/shared/types/CurrencyType.d.ts +4 -0
  31. package/src/features/shared/types/index.ts +4 -3
  32. package/src/features/shared/utils/budgetAmountParse.ts +24 -0
  33. package/src/features/shared/utils/constants.ts +1 -1
  34. package/src/features/users/hooks/useDebouncedSearchOrgUnit.ts +7 -11
package/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.3.19] - 2025-11-05
11
+
12
+ ### Added
13
+
14
+ - Add isLoading state to orgUnit search input
15
+
16
+ ## [1.3.18] - 2025-11-05
17
+
18
+ ### Added
19
+
20
+ - Responsiviness to Roles and Roles Details page
21
+
22
+ ### Added
23
+
24
+ - Notifications details on budget and integration
25
+
26
+ ### Added
27
+
28
+ - Add budget notification page
29
+
10
30
  ## [1.3.17] - 2025-11-03
11
31
 
12
32
  ### Added
@@ -106,13 +126,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
106
126
  ## [1.2.4] - 2025-10-16
107
127
 
108
128
  ### Added
109
-
110
- - Responsiviness to Roles and Roles Details page
111
-
112
- ### Added
113
-
114
129
  - Responsiviness adjustment to to Buying policies page
115
130
 
131
+
116
132
  ### Added
117
133
 
118
134
  - Responsiviness to Buying policies page
@@ -201,7 +217,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
201
217
  - Add CHANGELOG file
202
218
  - Add README file
203
219
 
204
- [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.17...HEAD
220
+ [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.19...HEAD
205
221
  [1.2.3]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.2.2...1.2.3
206
222
  [1.2.3]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.2.3
207
223
  [1.2.4]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.2.4
@@ -216,6 +232,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
216
232
 
217
233
  # <<<<<<< HEAD
218
234
 
235
+ [1.3.19]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.18...v1.3.19
236
+ [1.3.18]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.17...v1.3.18
219
237
  [1.3.17]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.16...v1.3.17
220
238
  [1.3.16]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.15...v1.3.16
221
239
  [1.3.15]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.14...v1.3.15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.3.17",
3
+ "version": "1.3.19",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -11,7 +11,7 @@ import {
11
11
  InputText,
12
12
  } from "../../../shared/components";
13
13
  import { useDebounce } from "../../../shared/hooks";
14
- import { useDeleteBudget } from "../../hooks";
14
+ import { useDeleteBudget } from "../../hooks/useDeleteBudget";
15
15
 
16
16
  export interface BudgetDeleteDrawerProps
17
17
  extends Omit<BasicDrawerProps, "children"> {
@@ -0,0 +1,139 @@
1
+ import { useCallback, useState } from "react";
2
+
3
+ import { useRouter } from "next/router";
4
+
5
+ import { useUI } from "@faststore/ui";
6
+
7
+ import { BasicDrawer, type BasicDrawerProps } from "../../../shared/components";
8
+ import { useBuyerPortal } from "../../../shared/hooks";
9
+ import { parseAmount } from "../../../shared/utils/budgetAmountParse";
10
+ import { useUpdateBudget } from "../../hooks";
11
+ import { BudgetNotificationForm } from "../BudgetNotificationForm/BudgetNotificationForm";
12
+
13
+ import type { BudgetInput, BudgetNotification } from "../../types";
14
+
15
+ interface BudgetEditNotificationDrawerProps
16
+ extends Omit<BasicDrawerProps, "children"> {
17
+ budget: BudgetInput;
18
+ budgetId: string;
19
+ orgUnitId: string;
20
+ contractId: string;
21
+ }
22
+
23
+ export function BudgetEditNotificationDrawer({
24
+ budget,
25
+ budgetId,
26
+ close,
27
+ orgUnitId,
28
+ contractId,
29
+ ...props
30
+ }: BudgetEditNotificationDrawerProps) {
31
+ const { pushToast } = useUI();
32
+ const { clientContext } = useBuyerPortal();
33
+ const router = useRouter();
34
+
35
+ const [notifications, setNotifications] = useState<
36
+ BudgetNotification | undefined
37
+ >(budget.notifications);
38
+ const [showNotificationsUsersError, setShowNotificationsUsersError] =
39
+ useState(false);
40
+
41
+ const { mutate: updateBudget, isLoading } = useUpdateBudget({
42
+ options: {
43
+ onSuccess: () => {
44
+ pushToast({ message: "Budget updated successfully.", status: "INFO" });
45
+ close();
46
+ router.reload();
47
+ },
48
+ onError: (error: Error) => {
49
+ pushToast({
50
+ message: error?.message || "An error occurred. Please try again.",
51
+ status: "ERROR",
52
+ });
53
+ },
54
+ },
55
+ });
56
+
57
+ const handleChange = useCallback(
58
+ <K extends keyof BudgetInput>(field: K, value: BudgetInput[K]) => {
59
+ if (field !== "notifications") return;
60
+
61
+ const next = value as BudgetInput["notifications"];
62
+
63
+ setNotifications(next);
64
+ },
65
+ [notifications]
66
+ );
67
+
68
+ const handleSubmit = () => {
69
+ const notificationsEnabled = Boolean(notifications?.hasNotification);
70
+ const hasUsers = (notifications?.users?.length ?? 0) > 0;
71
+
72
+ if (notificationsEnabled && !hasUsers) {
73
+ setShowNotificationsUsersError(true);
74
+ pushToast({
75
+ message: "Add at least one user to notifications or turn it off.",
76
+ status: "ERROR",
77
+ });
78
+ return;
79
+ }
80
+ setShowNotificationsUsersError(false);
81
+
82
+ const payload: BudgetInput = {
83
+ ...budget,
84
+ endDate: budget.endDate || budget.expirationDate,
85
+ amount: budget.amount ? String(budget.amount).replace(",", "") : "0",
86
+ allocations: budget?.allocations ?? [],
87
+ notifications: notifications ?? {
88
+ hasNotification: false,
89
+ thresholds: [],
90
+ users: [],
91
+ },
92
+ };
93
+
94
+ updateBudget({
95
+ budgetId,
96
+ cookie: clientContext.cookie,
97
+ customerId: contractId,
98
+ data: payload,
99
+ unitId: orgUnitId,
100
+ });
101
+ };
102
+
103
+ return (
104
+ <BasicDrawer
105
+ data-fs-bp-budget-notifications-drawer
106
+ close={close}
107
+ {...props}
108
+ >
109
+ <BasicDrawer.Heading title="Edit notifications" onClose={close} />
110
+
111
+ <BasicDrawer.Body>
112
+ <BudgetNotificationForm
113
+ budget={{ ...budget, notifications }}
114
+ handleChange={handleChange}
115
+ totalAmount={parseAmount(budget.amount)}
116
+ currency="USD"
117
+ locale="en-US"
118
+ unitId={orgUnitId}
119
+ contractId={contractId}
120
+ showUsersError={showNotificationsUsersError}
121
+ />
122
+ </BasicDrawer.Body>
123
+
124
+ <BasicDrawer.Footer data-fs-bp-budget-notifications-drawer-footer>
125
+ <BasicDrawer.Button variant="ghost" onClick={close}>
126
+ Cancel
127
+ </BasicDrawer.Button>
128
+ <BasicDrawer.Button
129
+ variant="confirm"
130
+ onClick={handleSubmit}
131
+ disabled={isLoading}
132
+ isLoading={isLoading}
133
+ >
134
+ Save
135
+ </BasicDrawer.Button>
136
+ </BasicDrawer.Footer>
137
+ </BasicDrawer>
138
+ );
139
+ }
@@ -0,0 +1,34 @@
1
+
2
+
3
+ [data-fs-bp-budget-notifications-drawer] {
4
+ @import "../../../shared/components/BasicDrawer/basic-drawer.scss";
5
+ @import "../../../shared/components/EmptyState/empty-state.scss";
6
+ @import "../../../shared/components/InputText/input-text.scss";
7
+ @import "../../../shared/components/ErrorMessage/error-message.scss";
8
+
9
+ &[data-fs-bp-basic-drawer] > [data-fs-bp-basic-drawer-footer] {
10
+ justify-content: flex-end;
11
+ gap: var(--fs-spacing-1);
12
+ }
13
+
14
+ &[data-fs-bp-basic-drawer][data-fs-slide-over-size="partial"] {
15
+ width: 100%;
16
+ max-width: none;
17
+
18
+ @include media(">=tablet") {
19
+ max-width: 30rem;
20
+ }
21
+
22
+ @include media(">=notebook") {
23
+ max-width: 40rem;
24
+ }
25
+ }
26
+
27
+ [data-fs-bp-basic-drawer-body] {
28
+ overflow-y: auto;
29
+ }
30
+
31
+ [data-fs-bp-budget-notifications-drawer-name] {
32
+ color: #0366dd;
33
+ }
34
+ }
@@ -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
+ };