@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.
- package/CHANGELOG.md +24 -6
- package/package.json +1 -1
- package/src/features/budgets/components/BudgetDeleteDrawer/BudgetDeleteDrawer.tsx +1 -1
- package/src/features/budgets/components/BudgetEditNotificationDrawer/BudgetEditNotificationDrawer.tsx +139 -0
- package/src/features/budgets/components/BudgetEditNotificationDrawer/budget-edit-notification-drawer.scss +34 -0
- package/src/features/budgets/components/BudgetNotificationForm/BudgetNotificationForm.tsx +361 -0
- package/src/features/budgets/components/BudgetNotificationForm/budget-notification-form.scss +219 -0
- package/src/features/budgets/components/BudgetNotificationsInfo/BudgetNotificationsInfo.tsx +116 -0
- package/src/features/budgets/components/BudgetNotificationsInfo/budget-notifications-info.scss +97 -0
- package/src/features/budgets/components/BudgetUsersTable/BudgetUsersTable.tsx +118 -0
- package/src/features/budgets/components/BudgetUsersTable/budget-users-table.scss +65 -0
- package/src/features/budgets/components/BudgetsTable/BudgetsTable.tsx +10 -0
- package/src/features/budgets/components/CreateBudgetAllocationDrawer/CreateBudgetAllocationDrawer.tsx +1 -1
- package/src/features/budgets/components/CreateBudgetDrawer/CreateBudgetDrawer.tsx +86 -25
- package/src/features/budgets/components/CreateBudgetDrawer/create-budget-drawer.scss +6 -0
- package/src/features/budgets/components/DeleteBudgetAllocationDrawer/DeleteBudgetAllocationDrawer.tsx +1 -1
- package/src/features/budgets/components/EditBudgetDrawer/EditBudgetDrawer.tsx +40 -1
- package/src/features/budgets/components/EditBudgetDrawer/edit-budget-drawer.scss +5 -0
- package/src/features/budgets/hooks/useDebouncedSearchBudgetNotification.ts +37 -0
- package/src/features/budgets/hooks/useListUsers.ts +1 -1
- package/src/features/budgets/layouts/BudgetsDetailsLayout/BudgetsDetailsLayout.tsx +9 -1
- package/src/features/budgets/layouts/BudgetsDetailsLayout/budget-details-layout.scss +14 -1
- package/src/features/budgets/layouts/BudgetsLayout/BudgetsLayout.tsx +39 -0
- package/src/features/budgets/layouts/BudgetsLayout/budgets-layout.scss +1 -1
- package/src/features/budgets/types/index.ts +17 -0
- package/src/features/shared/components/AutocompleteDropdown/AutocompleteDropdownItem.tsx +4 -0
- package/src/features/shared/components/OrgUnitInputSearch/OrgUnitInputSearch.tsx +17 -13
- package/src/features/shared/components/QuantitySelectorWithPercentage/QuantitySelectorWithPercentage.tsx +150 -0
- package/src/features/shared/components/index.ts +24 -23
- package/src/features/shared/types/CurrencyType.d.ts +4 -0
- package/src/features/shared/types/index.ts +4 -3
- package/src/features/shared/utils/budgetAmountParse.ts +24 -0
- package/src/features/shared/utils/constants.ts +1 -1
- 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.
|
|
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
|
@@ -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
|
+
};
|