@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
@@ -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;
@@ -0,0 +1,37 @@
1
+ import { useDebounce } from "../../shared/hooks";
2
+ import { DEBOUNCE_TIMEOUT } from "../../shared/utils";
3
+
4
+ import { useListUsers } from "./useListUsers";
5
+
6
+ type Params = {
7
+ customerId: string;
8
+ unitId: string;
9
+ search?: string;
10
+ };
11
+
12
+ export const useDebouncedSearchBudgetNotification = ({
13
+ customerId,
14
+ unitId,
15
+ search = "",
16
+ }: Params) => {
17
+ const debouncedSearchTerm = useDebounce(search, DEBOUNCE_TIMEOUT);
18
+ const isDebouncing = search !== debouncedSearchTerm;
19
+
20
+ const { listUsers, isListUsersLoading } = useListUsers({
21
+ keys: `${debouncedSearchTerm}`,
22
+ data: {
23
+ customerId,
24
+ unitId,
25
+ params: {
26
+ search: debouncedSearchTerm,
27
+ page: 1,
28
+ },
29
+ },
30
+ });
31
+
32
+ return {
33
+ usersResponse: listUsers,
34
+ isLoadingUsers: isListUsersLoading,
35
+ isDebouncing,
36
+ };
37
+ };
@@ -29,7 +29,7 @@ export const useListUsers = ({
29
29
  listUsersService({
30
30
  unitId,
31
31
  params: {
32
- budgetId,
32
+ ...(budgetId && { budgetId }),
33
33
  name: search,
34
34
  page,
35
35
  },
@@ -16,11 +16,12 @@ import { ContractTabsLayout, GlobalLayout } from "../../../shared/layouts";
16
16
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
17
17
  import { BudgetAllocationsTable } from "../../components/BudgetAllocationsTable/BudgetAllocationsTable";
18
18
  import { BudgetDeleteDrawer } from "../../components/BudgetDeleteDrawer/BudgetDeleteDrawer";
19
+ import { BudgetNotificationsInfo } from "../../components/BudgetNotificationsInfo/BudgetNotificationsInfo";
19
20
  import { BudgetRemainingBalance } from "../../components/BudgetRemainingBalance/BudgetRemainingBalance";
20
21
  import { BudgetSettingsInfoBalance } from "../../components/BudgetSettingsInfo/BudgetSettingsInfo";
21
22
  import { CreateBudgetAllocationDrawer } from "../../components/CreateBudgetAllocationDrawer/CreateBudgetAllocationDrawer";
22
23
  import { EditBudgetDrawer } from "../../components/EditBudgetDrawer/EditBudgetDrawer";
23
- import { useListAllocations } from "../../hooks";
24
+ import { useListAllocations } from "../../hooks/useListAllocations";
24
25
 
25
26
  import type { Budget, BudgetAllocationListResponse } from "../../types";
26
27
 
@@ -203,6 +204,13 @@ export const BudgetsDetailsLayout = ({ budget }: BudgetsDetailsLayoutProps) => {
203
204
  contractId={contract?.id ?? ""}
204
205
  />
205
206
 
207
+ <BudgetNotificationsInfo
208
+ initialBudget={budget}
209
+ budgetId={budget.id}
210
+ orgUnitId={orgUnit?.id ?? ""}
211
+ contractId={contract?.id ?? ""}
212
+ />
213
+
206
214
  {hasMounted && (
207
215
  <BudgetAllocationsTable
208
216
  loading={isAllocationsLoading && !loadingLoadMore}
@@ -4,12 +4,14 @@
4
4
  @import "../../components/BudgetAllocationsSelection/budget-allocations-selection.scss";
5
5
  @import "../../../budgets/components/DeleteBudgetAllocationDrawer/delete-budget-allocations.scss";
6
6
  @import "../../components/CreateBudgetAllocationDrawer/create-budget-allocation-drawer.scss";
7
+ @import "../../components/BudgetEditNotificationDrawer/budget-edit-notification-drawer.scss";
7
8
 
8
9
  [data-fs-bp-budgets-details-layout] {
9
10
  @import "../../components/CreateBudgetDrawer/create-budget-drawer.scss";
10
11
  @import "../../components/BudgetAddForm/budget-add-form.scss";
11
12
  @import "../../components/BudgetRemainingBalance/budget-remaining-balance.scss";
12
-
13
+ @import "../../components/BudgetNotificationForm/budget-notification-form.scss";
14
+ @import "../../components/BudgetNotificationsInfo/budget-notifications-info.scss";
13
15
  @import "../../components/BudgetSettingsInfo/budget-settings-info.scss";
14
16
  @import "../../../shared/components/InternalSearch/internal-search.scss";
15
17
  @import "../../components/BudgetsTable/budgets-table.scss";
@@ -37,6 +39,17 @@
37
39
  cursor: pointer;
38
40
  }
39
41
  }
42
+ [data-fs-bp-budget-notifications-empty] {
43
+ [data-fs-empty-state-section] {
44
+ padding: 4rem 17rem;
45
+ text-align: center;
46
+ color: #858585;
47
+
48
+ @include media("<=tablet") {
49
+ padding: 4rem clamp(1rem, 5vw, 17rem);
50
+ }
51
+ }
52
+ }
40
53
 
41
54
  [data-fs-bp-budgets-details-section] {
42
55
  min-height: calc(100vh - calc(var(--fs-spacing-9) + var(--fs-spacing-0)));
@@ -10,6 +10,7 @@ import {
10
10
  } from "../../../shared/hooks";
11
11
  import { FinanceTabsLayout, GlobalLayout } from "../../../shared/layouts";
12
12
  import { BudgetDeleteDrawer } from "../../components/BudgetDeleteDrawer/BudgetDeleteDrawer";
13
+ import { BudgetEditNotificationDrawer } from "../../components/BudgetEditNotificationDrawer/BudgetEditNotificationDrawer";
13
14
  import { BudgetsTable } from "../../components/BudgetsTable/BudgetsTable";
14
15
  import { CreateBudgetAllocationDrawer } from "../../components/CreateBudgetAllocationDrawer/CreateBudgetAllocationDrawer";
15
16
  import { CreateBudgetDrawer } from "../../components/CreateBudgetDrawer/CreateBudgetDrawer";
@@ -73,6 +74,12 @@ export const BudgetsLayout = ({ data }: BudgetsLayoutProps) => {
73
74
  ...createBudgetDrawerProps
74
75
  } = useDrawerProps();
75
76
 
77
+ const {
78
+ open: openNotificationDrawer,
79
+ isOpen: isNotificationDrawerOpen,
80
+ ...notificationDrawerProps
81
+ } = useDrawerProps();
82
+
76
83
  async function loadMore() {
77
84
  setPaginationLoading(true);
78
85
  setLoading(true);
@@ -134,6 +141,26 @@ export const BudgetsLayout = ({ data }: BudgetsLayoutProps) => {
134
141
  [page, contract?.id, orgUnit?.id, cookie]
135
142
  );
136
143
 
144
+ const handleCreateNotification = useCallback(
145
+ async (budgetId: string) => {
146
+ setSelectedBudgetId(budgetId);
147
+ try {
148
+ const budgetsData = await getBudgetByIdService({
149
+ budgetId,
150
+ customerId: contract?.id ?? "",
151
+ unitId: orgUnit?.id ?? "",
152
+ cookie,
153
+ });
154
+
155
+ setBudgetToEdit({ ...budgetsData, amount: String(budgetsData.amount) });
156
+ openNotificationDrawer();
157
+ } catch (error) {
158
+ console.error("Failed to load budget:", error);
159
+ }
160
+ },
161
+ [page, contract?.id, orgUnit?.id, cookie]
162
+ );
163
+
137
164
  return (
138
165
  <GlobalLayout>
139
166
  <FinanceTabsLayout pageName="Finance and Compliance">
@@ -164,6 +191,7 @@ export const BudgetsLayout = ({ data }: BudgetsLayoutProps) => {
164
191
  total={Number(total ?? 0)}
165
192
  onClickAllocationPage={handleAddAllocation}
166
193
  onClickEditBudget={handleBudgetEditPage}
194
+ onClickCreateNotification={handleCreateNotification}
167
195
  openDeleteDrawer={(budgetId) => {
168
196
  setSelectedBudgetId(budgetId);
169
197
  openDeleteDrawer();
@@ -232,6 +260,17 @@ export const BudgetsLayout = ({ data }: BudgetsLayoutProps) => {
232
260
  initialBudget={budgetsToEdit}
233
261
  />
234
262
  )}
263
+
264
+ {isNotificationDrawerOpen && budgetsToEdit && (
265
+ <BudgetEditNotificationDrawer
266
+ {...notificationDrawerProps}
267
+ isOpen={isNotificationDrawerOpen}
268
+ budget={budgetsToEdit}
269
+ budgetId={selectedBudgetId}
270
+ orgUnitId={orgUnit?.id ?? ""}
271
+ contractId={contract?.id ?? ""}
272
+ />
273
+ )}
235
274
  </section>
236
275
  </FinanceTabsLayout>
237
276
  </GlobalLayout>
@@ -4,7 +4,7 @@
4
4
  @import "@faststore/ui/src/components/molecules/Tooltip/styles.scss";
5
5
  @import "@faststore/ui/src/components/molecules/Table/styles.scss";
6
6
  @import "@faststore/ui/src/components/atoms/Button/styles.scss";
7
-
7
+ @import "../../components/BudgetNotificationForm/budget-notification-form.scss";
8
8
  @import "../../../shared/components/InternalSearch/internal-search.scss";
9
9
  @import "../../../shared/components/HeaderInside/header-inside.scss";
10
10
  @import "../../components/BudgetsTable/budgets-table.scss";
@@ -25,6 +25,22 @@ export type BudgetAllocationListResponse = {
25
25
  data: BudgetAllocation[];
26
26
  total: number;
27
27
  };
28
+
29
+ export type NotificationThresholds = {
30
+ value: number;
31
+ };
32
+ export type NotificationUsers = {
33
+ userId: string;
34
+ userName: string;
35
+ userEmail: string;
36
+ };
37
+
38
+ export type BudgetNotification = {
39
+ hasNotification: boolean;
40
+ thresholds: NotificationThresholds[];
41
+ users: NotificationUsers[];
42
+ };
43
+
28
44
  export type Budget = {
29
45
  id: string;
30
46
  name: string;
@@ -39,6 +55,7 @@ export type Budget = {
39
55
  preventCheckoutBudgetExceeded?: boolean;
40
56
  preventCheckoutBudgetExpired?: boolean;
41
57
  allocations?: BudgetAllocation[];
58
+ notifications?: BudgetNotification;
42
59
  };
43
60
 
44
61
  export type BudgetInputForm = Omit<BudgetInput, "amount"> & {
@@ -29,7 +29,11 @@ export const AutocompleteDropdownItem = ({
29
29
  }
30
30
  data-fs-bp-autocomplete-dropdown-option-selected={isSelected}
31
31
  onMouseEnter={() => setFocusedItemIndex(index)}
32
+ onMouseDown={(e) => {
33
+ e.preventDefault();
34
+ }}
32
35
  onClick={(e) => {
36
+ e.stopPropagation();
33
37
  if (closeOnClick) {
34
38
  close();
35
39
  }
@@ -4,6 +4,11 @@ import { AutocompleteDropdown } from "..";
4
4
  import { useDebouncedSearchOrgUnit } from "../../../users/hooks";
5
5
  import { OptionSelected } from "../OptionSelected/OptionSelected";
6
6
 
7
+ type Option = {
8
+ name: string;
9
+ id: string;
10
+ };
11
+
7
12
  export type OrgUnitInputSearchProps = Omit<
8
13
  ComponentProps<typeof AutocompleteDropdown>,
9
14
  "onSelect" | "label" | "onConfirmKeyPress"
@@ -25,9 +30,15 @@ export const OrgUnitInputSearch = ({
25
30
  }: OrgUnitInputSearchProps) => {
26
31
  const [autocompleteInputValue, setAutocompleteInputValue] = useState("");
27
32
 
28
- const { searchedOrgUnits } = useDebouncedSearchOrgUnit(
29
- autocompleteInputValue
30
- );
33
+ const { searchedOrgUnits, isSearchedOrgUnitsLoading } =
34
+ useDebouncedSearchOrgUnit(autocompleteInputValue);
35
+
36
+ const loadingObject: Option = { id: "loading", name: "loading..." };
37
+
38
+ const handleSelect = (option: Option) => {
39
+ if (option.id === loadingObject.id) return;
40
+ onSelect?.(option);
41
+ };
31
42
 
32
43
  return orgUnit.name ? (
33
44
  <OptionSelected
@@ -47,15 +58,8 @@ export const OrgUnitInputSearch = ({
47
58
  {...otherProps}
48
59
  label="Search organizational units"
49
60
  value={autocompleteInputValue}
50
- options={searchedOrgUnits}
51
- onConfirmKeyPress={(option) =>
52
- onSelect?.(
53
- option as {
54
- name: string;
55
- id: string;
56
- }
57
- )
58
- }
61
+ options={isSearchedOrgUnitsLoading ? [loadingObject] : searchedOrgUnits}
62
+ onConfirmKeyPress={handleSelect}
59
63
  onChange={(event) => {
60
64
  setAutocompleteInputValue(event.currentTarget.value);
61
65
  }}
@@ -65,7 +69,7 @@ export const OrgUnitInputSearch = ({
65
69
  closeOnClick
66
70
  index={index}
67
71
  isSelected={orgUnit?.id === option?.id}
68
- onClick={() => onSelect?.(option)}
72
+ onClick={() => handleSelect(option)}
69
73
  >
70
74
  {option?.name}
71
75
  </AutocompleteDropdown.Item>