@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,97 @@
1
+ [data-fs-bp-budget-notifications] {
2
+ @import "@faststore/ui/src/components/atoms/Badge/styles.scss";
3
+
4
+ width: 100%;
5
+
6
+ [data-fs-bp-budget-notifications-details-title] {
7
+ font-weight: var(--fs-text-weight-semibold);
8
+ font-size: var(--fs-text-size-2);
9
+ display: flex;
10
+ justify-content: space-between;
11
+ align-items: center;
12
+ padding: var(--fs-spacing-3) 0;
13
+ }
14
+
15
+ [data-fs-bp-budget-notifications-divider] {
16
+ border: calc(var(--fs-border-width) / 2) solid #e0e0e0;
17
+
18
+ &:first-of-type {
19
+ @include media("<tablet") {
20
+ border: none;
21
+ }
22
+ }
23
+ }
24
+
25
+ [data-fs-bp-budget-notifications-details-row] {
26
+ display: flex;
27
+ padding: 1.375rem 0;
28
+ font-weight: var(--fs-text-weight-medium);
29
+ font-size: var(--fs-text-size-1);
30
+ line-height: calc(var(--fs-spacing-3) + var(--fs-spacing-0));
31
+
32
+ @include media("<tablet") {
33
+ flex-direction: column;
34
+ padding: var(--fs-spacing-2) 0;
35
+ }
36
+
37
+ [data-fs-bp-budget-notifications-details-row-label] {
38
+ width: 12.5rem;
39
+ color: #707070;
40
+ font-size: var(--fs-text-size-1);
41
+ font-weight: var(--fs-text-weight-regular);
42
+
43
+ @include media("<tablet") {
44
+ font-size: var(--fs-text-size-0);
45
+ }
46
+ }
47
+
48
+ [data-fs-bp-budget-notifications-details-row-value] {
49
+ color: #000000;
50
+ font-size: var(--fs-text-size-1);
51
+ font-weight: var(--fs-text-weight-medium);
52
+ display: flex;
53
+ gap: var(--fs-spacing-1);
54
+
55
+ [data-fs-badge] {
56
+ color: #000000;
57
+ font-size: var(--fs-text-size-0);
58
+ font-weight: var(--fs-text-weight-regular);
59
+ }
60
+
61
+ @include media("<tablet") {
62
+ font-weight: var(--fs-text-weight-regular);
63
+ }
64
+ }
65
+ }
66
+
67
+ [data-fs-budget-notifications-edit-line] {
68
+ padding: calc(var(--fs-spacing-4) - var(--fs-spacing-0)) 0;
69
+ border-top: var(--fs-border-width) solid #e0e0e0;
70
+ display: flex;
71
+ align-items: center;
72
+
73
+ &:first-of-type {
74
+ border-top: none;
75
+ }
76
+
77
+ [data-fs-budget-notifications-edit-title] {
78
+ font-weight: var(--fs-text-weight-semibold);
79
+ font-size: var(--fs-text-size-2);
80
+ line-height: var(--fs-spacing-4);
81
+ margin-right: auto;
82
+ }
83
+
84
+ [data-fs-budget-notifications-edit-button] {
85
+ border: var(--fs-border-width) solid #e0e0e0;
86
+ border-radius: var(--fs-border-radius-pill);
87
+ padding: var(--fs-spacing-1)
88
+ calc(var(--fs-spacing-4) - var(--fs-spacing-0));
89
+ font-weight: var(--fs-text-weight-semibold);
90
+ font-size: var(--fs-text-size-1);
91
+ line-height: calc(var(--fs-spacing-4) - var(--fs-spacing-0));
92
+ text-align: center;
93
+ color: #0366dd;
94
+ cursor: pointer;
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,118 @@
1
+ import { memo } from "react";
2
+
3
+ import { Icon, Tooltip } from "@faststore/ui";
4
+
5
+ import { Table } from "../../../shared/components";
6
+ import { getTableColumns } from "../../../shared/components/Table/utils/tableColumns";
7
+ import { useBuyerPortal } from "../../../shared/hooks";
8
+
9
+ type UserRow = {
10
+ userId: string;
11
+ userName: string;
12
+ email: string;
13
+ removable?: boolean;
14
+ };
15
+
16
+ type BudgetUsersTableProps = {
17
+ users: UserRow[];
18
+ onRemove?: (userId: string) => void;
19
+ emptyMessage?: string;
20
+ className?: string;
21
+ disabled?: boolean;
22
+ };
23
+
24
+ function BudgetUsersTableBase({
25
+ users,
26
+ onRemove,
27
+ emptyMessage = "No users found",
28
+ className,
29
+ disabled = false,
30
+ }: BudgetUsersTableProps) {
31
+ const { clientContext } = useBuyerPortal();
32
+ const currClientId = clientContext.userId;
33
+ const columns = getTableColumns({
34
+ actionsLength: onRemove ? 1 : 0,
35
+ nameColumnLabel: "User name",
36
+ nameColumnKey: "name",
37
+ nameColumnSize: "medium",
38
+ extraColumns: [
39
+ {
40
+ key: "email",
41
+ label: "Email",
42
+ align: "left",
43
+ size: "100%",
44
+ hideOnScreenSize: "phonemid",
45
+ },
46
+ ],
47
+ });
48
+
49
+ return (
50
+ <section
51
+ data-fs-bp-users-table
52
+ className={className}
53
+ aria-busy={disabled ? "true" : "false"}
54
+ >
55
+ <Table layoutFixed>
56
+ <Table.Head columns={columns} />
57
+
58
+ <Table.Body>
59
+ {users.length === 0 ? (
60
+ <Table.Row title={emptyMessage} enabled={false}>
61
+ <Table.Cell />
62
+ </Table.Row>
63
+ ) : (
64
+ users.map((u) => (
65
+ <Table.Row
66
+ key={u.userId}
67
+ title={
68
+ currClientId === u.userId ? (
69
+ <span>
70
+ {u.userName}
71
+ <span data-fs-bp-users-you> (you)</span>
72
+ </span>
73
+ ) : (
74
+ u.userName
75
+ )
76
+ }
77
+ enabled={!disabled}
78
+ actionIcons={(() => {
79
+ const rowDisabled =
80
+ disabled || users.length === 1 || !onRemove;
81
+ const button = (
82
+ <button
83
+ type="button"
84
+ data-fs-bp-users-remove
85
+ aria-label={`Remove ${u.userName}`}
86
+ disabled={rowDisabled}
87
+ onClick={() => onRemove?.(u.userId)}
88
+ >
89
+ <Icon name="MinusCircle" width={20} height={20} />
90
+ </button>
91
+ );
92
+
93
+ return rowDisabled && users.length === 1 ? (
94
+ <Tooltip
95
+ placement="left-center"
96
+ content="At least one user must be notified. Add another user before removing this one."
97
+ >
98
+ {button}
99
+ </Tooltip>
100
+ ) : (
101
+ button
102
+ );
103
+ })()}
104
+ >
105
+ <Table.Cell>
106
+ <span data-fs-bp-users-email>{u.email}</span>
107
+ </Table.Cell>
108
+ </Table.Row>
109
+ ))
110
+ )}
111
+ </Table.Body>
112
+ </Table>
113
+ </section>
114
+ );
115
+ }
116
+
117
+ export const BudgetUsersTable = memo(BudgetUsersTableBase);
118
+ export default BudgetUsersTable;
@@ -0,0 +1,65 @@
1
+
2
+ [data-fs-bp-users-table] {
3
+ @import "../../../shared/components/Table/table.scss";
4
+ display: block;
5
+ gap: 0;
6
+ margin-top: var(--fs-spacing-3);
7
+ width: 100%;
8
+ background: #fff;
9
+ overflow: hidden;
10
+
11
+ [data-fs-bp-table] {
12
+ [data-fs-bp-table-head] {
13
+ [data-fs-bp-table-head-row] {
14
+ border-bottom: var(--fs-border-radius-small) solid #e0e0e0;
15
+
16
+ th[data-fs-bp-table-head-column] {
17
+ text-align: start;
18
+ font-weight: var(--fs-text-weight-medium);
19
+ font-size: var(--fs-text-size-1);
20
+ color: #5c5c5c;
21
+ padding: var(--fs-spacing-2) 0;
22
+ }
23
+
24
+ }
25
+ }
26
+ }
27
+
28
+ [data-fs-bp-users-you] {
29
+ color: #6b7280;
30
+ margin-left: 0.25rem;
31
+ }
32
+
33
+ [data-fs-bp-users-email] {
34
+ color: #374151;
35
+ font-size: var(--fs-text-size-1);
36
+ word-break: break-word;
37
+ }
38
+
39
+ [data-fs-bp-users-remove] {
40
+ display: inline-flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+
44
+ border-radius: 9999px;
45
+ background: #fff;
46
+ color: #111827;
47
+ transition: box-shadow 0.15s ease, background 0.15s ease;
48
+ cursor: pointer;
49
+
50
+ svg {
51
+ color: #000000;
52
+ stroke-width: 1.3rem;
53
+ width: 1.25rem;
54
+ height: 1.25rem;
55
+ }
56
+
57
+ &[disabled] {
58
+ cursor: not-allowed;
59
+ svg {
60
+ color: #9ca3af;
61
+ }
62
+ }
63
+ }
64
+
65
+ }
@@ -20,6 +20,7 @@ interface IBudgetsTable {
20
20
  onClickEditBudget(budgetId: string): void;
21
21
  openDeleteDrawer(budgetId: string): void;
22
22
  loading: boolean;
23
+ onClickCreateNotification?(budgetId: string): void;
23
24
  }
24
25
 
25
26
  export function BudgetsTable({
@@ -28,6 +29,7 @@ export function BudgetsTable({
28
29
  onClickEditBudget,
29
30
  openDeleteDrawer,
30
31
  loading,
32
+ onClickCreateNotification,
31
33
  }: IBudgetsTable) {
32
34
  const budgets = data;
33
35
  const route = useRouter();
@@ -96,6 +98,14 @@ export function BudgetsTable({
96
98
  <Icon name="Edit" width={20} height={20} />
97
99
  <span>Edit settings</span>
98
100
  </DropdownItem>
101
+ {onClickCreateNotification && (
102
+ <DropdownItem
103
+ onClick={() => onClickCreateNotification(item.id)}
104
+ >
105
+ <Icon name="Edit" width={20} height={20} />
106
+ <span>Edit notifications</span>
107
+ </DropdownItem>
108
+ )}
99
109
  <DropdownItem
100
110
  onClick={() => onClickAllocationPage(item.id)}
101
111
  >
@@ -10,7 +10,7 @@ import {
10
10
  import { useBuyerPortal } from "../../../shared/hooks";
11
11
  import { sortingOptionsAllocations } from "../../../shared/utils";
12
12
  import { getKeyByValue } from "../../../shared/utils/getKeyByValue";
13
- import { useAddAllocations } from "../../hooks";
13
+ import { useAddAllocations } from "../../hooks/useAddAllocations";
14
14
  import { useGetAllocations } from "../../hooks/useGetAllocations";
15
15
  import { BudgetAllocationsSelection } from "../BudgetAllocationsSelection/BudgetAllocationsSelection";
16
16
 
@@ -11,14 +11,18 @@ import {
11
11
  } from "../../../shared/components";
12
12
  import { useBuyerPortal } from "../../../shared/hooks";
13
13
  import { sortingOptionsAllocations } from "../../../shared/utils";
14
+ import { parseAmount } from "../../../shared/utils/budgetAmountParse";
14
15
  import { getKeyByValue } from "../../../shared/utils/getKeyByValue";
15
- import { useCreateBudget, useUpdateBudget } from "../../hooks";
16
+ import { useCreateBudget } from "../../hooks/useCreateBudget";
16
17
  import { useGetAllocations } from "../../hooks/useGetAllocations";
18
+ import { useUpdateBudget } from "../../hooks/useUpdateBudget";
17
19
  import { listBudgetsService } from "../../services";
18
- import { BudgetInput, BudgetList } from "../../types";
19
20
  import { BudgetAddForm } from "../BudgetAddForm/BudgetAddForm";
20
21
  import { BudgetAddSuccess } from "../BudgetAddSuccess/BudgetAddSuccess";
21
22
  import { BudgetAllocationsSelection } from "../BudgetAllocationsSelection/BudgetAllocationsSelection";
23
+ import { BudgetNotificationForm } from "../BudgetNotificationForm/BudgetNotificationForm";
24
+
25
+ import type { BudgetInput, BudgetList, BudgetListResponse } from "../../types";
22
26
 
23
27
  const createTouchedState = (): Record<keyof BudgetInput, boolean> => ({
24
28
  name: false,
@@ -31,6 +35,7 @@ const createTouchedState = (): Record<keyof BudgetInput, boolean> => ({
31
35
  allocations: false,
32
36
  preventCheckoutBudgetExceeded: false,
33
37
  preventCheckoutBudgetExpired: false,
38
+ notifications: false,
34
39
  });
35
40
 
36
41
  const isBudgetValid = (budget: BudgetInput) => {
@@ -93,6 +98,7 @@ export const CreateBudgetDrawer = ({
93
98
  preventCheckoutBudgetExpired: false,
94
99
  description: "",
95
100
  allocations: [],
101
+ notifications: undefined,
96
102
  });
97
103
 
98
104
  const [touched, setTouched] = useState(createTouchedState);
@@ -101,6 +107,8 @@ export const CreateBudgetDrawer = ({
101
107
  const [blockOnExpiration, setBlockOnExpiration] = useState(true);
102
108
  const [listBudgetsLoading, setListBudgetsLoading] = useState(false);
103
109
  const [firstBudget, setFirstBudget] = useState<BudgetList | null>(null);
110
+ const [showNotificationsUsersError, setShowNotificationsUsersError] =
111
+ useState(false);
104
112
 
105
113
  const { mutate: createBudget, isLoading: isCreatingBudget } = useCreateBudget(
106
114
  {
@@ -108,13 +116,15 @@ export const CreateBudgetDrawer = ({
108
116
  onSuccess: async () => {
109
117
  setListBudgetsLoading(true);
110
118
 
111
- const { data, total } = await listBudgetsService({
119
+ const res = await listBudgetsService({
112
120
  customerId: contractId,
113
121
  unitId,
114
122
  page: 1,
115
123
  cookie,
116
124
  }).finally(() => setListBudgetsLoading(false));
117
125
 
126
+ const { data, total } = res as BudgetListResponse;
127
+
118
128
  if (total === 1) {
119
129
  setStep("confirmation");
120
130
  setFirstBudget(data[0]);
@@ -129,7 +139,7 @@ export const CreateBudgetDrawer = ({
129
139
  close();
130
140
  router.reload();
131
141
  },
132
- onError: (error) =>
142
+ onError: (error: Error) =>
133
143
  pushToast({ message: error.message, status: "ERROR" }),
134
144
  },
135
145
  }
@@ -156,7 +166,7 @@ export const CreateBudgetDrawer = ({
156
166
  close();
157
167
  router.reload();
158
168
  },
159
- onError: (error) => {
169
+ onError: (error: Error) => {
160
170
  pushToast({
161
171
  message: error?.message || "An error occurred. Please try again.",
162
172
  status: "ERROR",
@@ -166,15 +176,11 @@ export const CreateBudgetDrawer = ({
166
176
  });
167
177
 
168
178
  const handleChange = useCallback(
169
- (field: keyof BudgetInput, value: string | boolean) => {
179
+ <K extends keyof BudgetInput>(field: K, value: BudgetInput[K]) => {
170
180
  if (field === "amount" && typeof value === "string") {
171
- setBudget((prev) => ({
172
- ...prev,
173
- amount: value,
174
- }));
181
+ setBudget((prev) => ({ ...prev, amount: value }));
175
182
  return;
176
183
  }
177
-
178
184
  if (
179
185
  (field === "startDate" || field === "expirationDate") &&
180
186
  typeof value === "string" &&
@@ -208,18 +214,24 @@ export const CreateBudgetDrawer = ({
208
214
  pushToast({ message: "Fill in all required fields.", status: "ERROR" });
209
215
  return;
210
216
  }
217
+ const hasUsers = (budget?.notifications?.users?.length ?? 0) > 0;
218
+ const hasThresholds = (budget?.notifications?.thresholds?.length ?? 0) > 0;
211
219
 
212
- const payload = {
220
+ const payload: BudgetInput = {
213
221
  ...budget,
222
+ ...(hasUsers &&
223
+ hasThresholds && {
224
+ notifications: budget.notifications,
225
+ }),
214
226
  amount: budget.amount ? budget.amount.replace(",", "") : "0",
215
227
  allocations: skipAllocations
216
228
  ? []
217
- : selectedAllocations.map((allocation) => ({
229
+ : (selectedAllocations.map((allocation) => ({
218
230
  id: ["USER", "ADDRESS"].includes(allocation.type)
219
231
  ? allocation.id
220
232
  : allocation.name,
221
233
  type: allocation.type,
222
- })),
234
+ })) as NonNullable<BudgetInput["allocations"]>),
223
235
  preventCheckoutBudgetExceeded: false,
224
236
  preventCheckoutBudgetExpired: false,
225
237
  };
@@ -241,9 +253,12 @@ export const CreateBudgetDrawer = ({
241
253
  description: budget.description,
242
254
  startDate: budget.startDate,
243
255
  endDate: budget.endDate,
256
+ expirationDate: budget.expirationDate,
244
257
  autoResetOnPeriodEnd: budget.autoResetOnPeriodEnd,
245
258
  preventCheckoutBudgetExceeded: enforcePolicy ? blockOnExceed : false,
246
259
  preventCheckoutBudgetExpired: enforcePolicy ? blockOnExpiration : false,
260
+ allocations: [],
261
+ notifications: budget.notifications,
247
262
  };
248
263
 
249
264
  updateBudget({
@@ -280,18 +295,31 @@ export const CreateBudgetDrawer = ({
280
295
  }
281
296
  onClose={close}
282
297
  />
283
-
284
298
  <BasicDrawer.Body>
285
299
  {step === "form" && (
286
- <BudgetAddForm
287
- budget={budget}
288
- setBudget={setBudget}
289
- touched={touched}
290
- setTouched={setTouched}
291
- handleChange={handleChange}
292
- handleBlur={handleBlur}
293
- readonly={readonly}
294
- />
300
+ <>
301
+ <BudgetAddForm
302
+ budget={budget}
303
+ setBudget={setBudget}
304
+ touched={touched}
305
+ setTouched={setTouched}
306
+ handleChange={handleChange}
307
+ handleBlur={handleBlur}
308
+ readonly={readonly}
309
+ />
310
+ <div data-fs-divider />
311
+ <BudgetNotificationForm
312
+ budget={budget}
313
+ contractId={contractId}
314
+ unitId={unitId}
315
+ handleChange={handleChange}
316
+ totalAmount={parseAmount(budget.amount)}
317
+ currency="USD"
318
+ locale="en-US"
319
+ readonly={readonly}
320
+ showUsersError={showNotificationsUsersError}
321
+ />
322
+ </>
295
323
  )}
296
324
 
297
325
  {step === "allocations" && (
@@ -337,7 +365,40 @@ export const CreateBudgetDrawer = ({
337
365
  </BasicDrawer.Button>
338
366
  <BasicDrawer.Button
339
367
  variant="confirm"
340
- onClick={() => setStep("allocations")}
368
+ onClick={() => {
369
+ const notificationsEnabled = Boolean(
370
+ budget?.notifications?.hasNotification
371
+ );
372
+ const hasUsers =
373
+ (budget?.notifications?.users?.length ?? 0) > 0;
374
+ if (!isBudgetValid(budget)) {
375
+ setTouched(
376
+ Object.keys(budget).reduce(
377
+ (acc, key) => ({ ...acc, [key]: true }),
378
+ {} as Record<keyof BudgetInput, boolean>
379
+ )
380
+ );
381
+ if (notificationsEnabled && !hasUsers)
382
+ setShowNotificationsUsersError(true);
383
+ pushToast({
384
+ message: "Fill in all required fields.",
385
+ status: "ERROR",
386
+ });
387
+ return;
388
+ }
389
+
390
+ if (notificationsEnabled && !hasUsers) {
391
+ setShowNotificationsUsersError(true);
392
+ pushToast({
393
+ message:
394
+ "Add at least one user to notifications or turn it off.",
395
+ status: "ERROR",
396
+ });
397
+ return;
398
+ }
399
+ setShowNotificationsUsersError(false);
400
+ setStep("allocations");
401
+ }}
341
402
  >
342
403
  Continue
343
404
  </BasicDrawer.Button>
@@ -1,6 +1,7 @@
1
1
  @import "@faststore/ui/src/components/molecules/Toggle/styles.scss";
2
2
  @import "@faststore/ui/src/components/molecules/Tooltip/styles.scss";
3
3
  @import "../BudgetAddForm/budget-add-form.scss";
4
+ @import "../BudgetNotificationForm/budget-notification-form.scss";
4
5
  @import "../BudgetAllocationsSelection/budget-allocations-selection.scss";
5
6
  @import "../BudgetAddSuccess/budget-add-success.scss";
6
7
 
@@ -29,6 +30,11 @@
29
30
  max-width: 40rem;
30
31
  }
31
32
 
33
+ [data-fs-divider] {
34
+ border: var(--fs-border-radius-small) solid #e0e0e0;
35
+ margin: var(--fs-spacing-4) 0rem;
36
+ }
37
+
32
38
  & > [data-fs-bp-basic-drawer-footer] {
33
39
  justify-content: flex-end;
34
40
  gap: var(--fs-spacing-1);
@@ -10,7 +10,7 @@ import {
10
10
  type BasicDrawerProps,
11
11
  } from "../../../shared/components";
12
12
  import { useBuyerPortal } from "../../../shared/hooks";
13
- import { useDeleteAllocation } from "../../hooks";
13
+ import { useDeleteAllocation } from "../../hooks/useDeleteAllocations";
14
14
 
15
15
  import type { BudgetAllocation } from "../../types";
16
16
 
@@ -6,8 +6,10 @@ import { useUI } from "@faststore/ui";
6
6
 
7
7
  import { BasicDrawer, type BasicDrawerProps } from "../../../shared/components";
8
8
  import { useBuyerPortal } from "../../../shared/hooks";
9
+ import { parseAmount } from "../../../shared/utils/budgetAmountParse";
9
10
  import { useUpdateBudget } from "../../hooks";
10
11
  import { BudgetAddForm } from "../BudgetAddForm/BudgetAddForm";
12
+ import { BudgetNotificationForm } from "../BudgetNotificationForm/BudgetNotificationForm";
11
13
 
12
14
  import type { BudgetInput } from "../../types";
13
15
 
@@ -22,6 +24,7 @@ const getInitialBudget = (initial?: BudgetInput): BudgetInput => ({
22
24
  preventCheckoutBudgetExceeded: false,
23
25
  preventCheckoutBudgetExpired: false,
24
26
  allocations: [],
27
+ notifications: initial?.notifications,
25
28
  ...initial,
26
29
  });
27
30
 
@@ -36,6 +39,7 @@ const createTouchedObject = (): Record<keyof BudgetInput, boolean> => ({
36
39
  preventCheckoutBudgetExceeded: false,
37
40
  preventCheckoutBudgetExpired: false,
38
41
  allocations: false,
42
+ notifications: false,
39
43
  });
40
44
 
41
45
  const isBudgetValid = (budget: BudgetInput) => {
@@ -76,6 +80,8 @@ export const EditBudgetDrawer = ({
76
80
  getInitialBudget(initialBudget)
77
81
  );
78
82
  const [touched, setTouched] = useState(createTouchedObject);
83
+ const [showNotificationsUsersError, setShowNotificationsUsersError] =
84
+ useState(false);
79
85
 
80
86
  const { mutate: updateBudget, isLoading } = useUpdateBudget({
81
87
  options: {
@@ -85,7 +91,7 @@ export const EditBudgetDrawer = ({
85
91
  close();
86
92
  router.reload();
87
93
  },
88
- onError: (error) => {
94
+ onError: (error: Error) => {
89
95
  pushToast({
90
96
  message: error?.message || "An error occurred. Please try again.",
91
97
  status: "ERROR",
@@ -117,6 +123,7 @@ export const EditBudgetDrawer = ({
117
123
  preventCheckoutBudgetExceeded: true,
118
124
  preventCheckoutBudgetExpired: true,
119
125
  allocations: true,
126
+ notifications: true,
120
127
  });
121
128
 
122
129
  if (!isBudgetValid(budget)) {
@@ -127,8 +134,28 @@ export const EditBudgetDrawer = ({
127
134
  return;
128
135
  }
129
136
 
137
+ const notificationsEnabled = Boolean(
138
+ budget?.notifications?.hasNotification
139
+ );
140
+ const hasUsers = (budget?.notifications?.users?.length ?? 0) > 0;
141
+ if (notificationsEnabled && !hasUsers) {
142
+ setShowNotificationsUsersError(true);
143
+ pushToast({
144
+ message: "Add at least one user to notifications or turn it off.",
145
+ status: "ERROR",
146
+ });
147
+ return;
148
+ }
149
+ setShowNotificationsUsersError(false);
150
+
151
+ const hasThresholds = (budget?.notifications?.thresholds?.length ?? 0) > 0;
152
+
130
153
  const payload: BudgetInput = {
131
154
  ...budget,
155
+ ...(hasUsers &&
156
+ hasThresholds && {
157
+ notifications: budget.notifications,
158
+ }),
132
159
  endDate: budget.endDate || budget.expirationDate,
133
160
  amount: budget.amount ? String(budget.amount).replace(",", "") : "0",
134
161
  allocations: initialBudget?.allocations ?? [],
@@ -157,6 +184,18 @@ export const EditBudgetDrawer = ({
157
184
  handleBlur={handleBlur}
158
185
  readonly={readonly}
159
186
  />
187
+ <div data-fs-divider />
188
+ <BudgetNotificationForm
189
+ budget={budget}
190
+ contractId={contractId}
191
+ unitId={orgUnitId}
192
+ handleChange={handleChange}
193
+ totalAmount={parseAmount(budget.amount)}
194
+ currency="USD"
195
+ locale="en-US"
196
+ readonly={readonly}
197
+ showUsersError={showNotificationsUsersError}
198
+ />
160
199
  </BasicDrawer.Body>
161
200
 
162
201
  <BasicDrawer.Footer>
@@ -3,6 +3,7 @@
3
3
  @import "@faststore/ui/src/components/molecules/Tooltip/styles.scss";
4
4
  @import "../../../shared/components/BasicDrawer/basic-drawer.scss";
5
5
  @import "../BudgetAddForm/budget-add-form.scss";
6
+ @import "../BudgetNotificationForm/budget-notification-form.scss";
6
7
  @import "../../../shared/components/InputText/input-text.scss";
7
8
  @import "../../../shared/components/ErrorMessage/error-message.scss";
8
9
 
@@ -31,6 +32,10 @@
31
32
  width: 100%;
32
33
  }
33
34
  }
35
+ [data-fs-divider] {
36
+ border: var(--fs-border-radius-small) solid #e0e0e0;
37
+ margin: var(--fs-spacing-4) 0rem;
38
+ }
34
39
 
35
40
  footer[data-fs-bp-basic-drawer-footer] {
36
41
  justify-content: flex-end;