@vtex/faststore-plugin-buyer-portal 2.0.10 → 2.0.11

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 +15 -1
  2. package/package.json +1 -1
  3. package/src/features/addresses/clients/AddressesClient.ts +15 -11
  4. package/src/features/addresses/components/CreateAddressSettingsDrawer/CreateAddressSettingsDrawer.tsx +1 -0
  5. package/src/features/addresses/hooks/useDebouncedSearchAddressByUnitId.ts +2 -0
  6. package/src/features/addresses/hooks/useListAddresses.ts +15 -5
  7. package/src/features/addresses/layouts/AddressesLayout/AddressesLayout.tsx +1 -0
  8. package/src/features/addresses/services/{get-addresses-by-unit-id.service.ts → get-attached-addresses-by-customer-id.service.ts} +18 -14
  9. package/src/features/addresses/services/index.ts +3 -3
  10. package/src/features/addresses/types/AddressData.ts +3 -1
  11. package/src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx +39 -5
  12. package/src/features/contracts/layouts/ContractListingLayout/contract-listing-layout.scss +2 -2
  13. package/src/features/org-units/layouts/OrgUnitDetailsLayout/OrgUnitDetailsLayout.tsx +4 -1
  14. package/src/features/shared/clients/Client.ts +1 -1
  15. package/src/features/shared/hooks/index.ts +1 -0
  16. package/src/features/shared/hooks/useReloadOnBfcacheRestore.ts +24 -0
  17. package/src/features/shared/layouts/BaseTabsLayout/SidebarMenu.tsx +8 -8
  18. package/src/features/shared/layouts/ContractTabsLayout/ContractTabsLayout.tsx +4 -1
  19. package/src/features/shared/layouts/OrgUnitTabsLayout/OrgUnitTabLayout.tsx +4 -1
  20. package/src/features/shared/layouts/SumaPageLayout/FullSidebarNav.tsx +17 -6
  21. package/src/features/shared/layouts/SumaPageLayout/SumaPageLayout.tsx +13 -3
  22. package/src/features/shared/layouts/SumaPageLayout/SumaSidebar.tsx +3 -0
  23. package/src/features/shared/utils/__tests__/resolveContractId.test.ts +47 -0
  24. package/src/features/shared/utils/buyerPortalRoutes.ts +29 -7
  25. package/src/features/shared/utils/constants.ts +1 -1
  26. package/src/features/shared/utils/getOrganizationSettingsLinks.ts +10 -4
  27. package/src/features/shared/utils/getSumaListingSidebarLinks.ts +1 -1
  28. package/src/features/shared/utils/index.ts +1 -0
  29. package/src/features/shared/utils/resolveContractId.ts +30 -0
  30. package/src/features/shared/utils/withAuthLoader.ts +11 -3
  31. package/src/pages/contracts.tsx +28 -26
  32. package/src/pages/org-units.tsx +20 -1
  33. package/src/pages/roles.tsx +32 -14
  34. package/src/pages/users.tsx +41 -18
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.11] - 2026-07-17
11
+
12
+ ### Fixed
13
+
14
+ - Sidebar links on the Users, Roles, and Organizational Units pages now carry the currently viewed contract's id in the URL, instead of silently falling back to the org unit's first contract — this previously caused the Finance section (Budgets/Buying Policies) links and accounting fields on those pages to point at the wrong contract
15
+ - Removed a redundant `getBuyerPortalFeatureFlags` fetch on the Contract Listing page and a conflicting `no-cache`/`next.revalidate: 300` combination on the shared HTTP client, both of which could intermittently hide the "Add contracts" button (and serve stale per-contract data) for up to 5 minutes
16
+ - Removing the session's currently active contract from an organizational unit now switches the session to a remaining contract, instead of leaving the UI pointed at a contract that no longer exists
17
+ - Added a back-forward-cache guard to the contract switch flow to address a QA report of briefly seeing the previous contract's data after switching and going back
18
+ - `SumaPageLayout` (the layout actually rendering the Users/Roles/Organizational Units sidebar when the `suma` flag is on) now also resolves its contract id from the URL instead of falling back to the org unit's first contract; the Users/Roles/Organizational Units page loaders now fetch minimal contract details when the URL carries a contract id, so the sidebar identity block shows consistently everywhere, not just on Budgets/Buying Policies
19
+ - The sidebar's "Accounting fields" fetch is now skipped while its containing layout is in a transient loading state (e.g. during a route transition), instead of firing with route-parsed, possibly-stale ids; the link itself still renders, just without its child fields until loading finishes
20
+ - Widened the Contract Listing sidebar and adjusted its padding; the "Add contracts" button is no longer gated behind an admin role check
21
+
10
22
  ## [2.0.10] - 2026-07-17
11
23
 
12
24
  ### Changed
@@ -745,7 +757,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
745
757
  - Add CHANGELOG file
746
758
  - Add README file
747
759
 
748
- [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.10...HEAD
760
+ [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/2.0.11...HEAD
749
761
  [1.3.55]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.54...v1.3.55
750
762
  [1.3.54]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.53...v1.3.54
751
763
  [1.3.53]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.52...v1.3.53
@@ -842,3 +854,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
842
854
 
843
855
  [2.0.10]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.9...v2.0.10
844
856
  [2.0.9]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/2.0.9
857
+
858
+ [2.0.11]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/2.0.11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "2.0.10",
3
+ "version": "2.0.11",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -9,7 +9,7 @@ import type {
9
9
  } from "../types";
10
10
  import type {
11
11
  EditLocationSettingsParams,
12
- GetAddressesByUnitIdParams,
12
+ GetAttachedAddressesByCustomerIdParams,
13
13
  } from "../types/AddressData";
14
14
 
15
15
  type AddressResponse = {
@@ -33,21 +33,25 @@ export default class AddressesClient extends Client {
33
33
  );
34
34
  }
35
35
 
36
- async getAddressesByUnitId({
36
+ async getAttachedAddressesByCustomerId({
37
37
  orgUnitId,
38
+ customerId,
38
39
  cookie,
39
40
  name,
40
41
  addressType,
41
42
  page = 1,
42
- }: GetAddressesByUnitIdParams) {
43
- return this.get<AddressResponse>(`units/${orgUnitId}/addresses`, {
44
- headers: { Cookie: cookie },
45
- params: {
46
- page,
47
- name: name ?? "",
48
- addressType: addressType ?? "",
49
- },
50
- });
43
+ }: GetAttachedAddressesByCustomerIdParams) {
44
+ return this.get<AddressResponse>(
45
+ `units/${orgUnitId}/customers/${customerId}/addresses/attached`,
46
+ {
47
+ headers: { Cookie: cookie },
48
+ params: {
49
+ page,
50
+ name: name ?? "",
51
+ addressType: addressType ?? "",
52
+ },
53
+ }
54
+ );
51
55
  }
52
56
 
53
57
  getAddressById(
@@ -91,6 +91,7 @@ export const CreateAddressSettingsDrawer = ({
91
91
 
92
92
  const { searchedAddresses } = useDebouncedSearchAddressByUnitId({
93
93
  orgUnitId: router.query.orgUnitId as string,
94
+ contractId: router.query.contractId as string,
94
95
  search: searchValue,
95
96
  addressType: activeField === "Shipping" ? "Shipping" : "Billing",
96
97
  });
@@ -8,12 +8,14 @@ import type { UseDebouncedSearchAddressByUnitIdProps } from "../types/AddressDat
8
8
 
9
9
  export const useDebouncedSearchAddressByUnitId = ({
10
10
  orgUnitId,
11
+ contractId,
11
12
  search = "",
12
13
  addressType,
13
14
  }: UseDebouncedSearchAddressByUnitIdProps) => {
14
15
  const debouncedSearchTerm = useDebounce(search, DEBOUNCE_TIMEOUT);
15
16
  const { listAddressesData, listAddressesLoading } = useListAddresses({
16
17
  orgUnitId,
18
+ contractId,
17
19
  search: debouncedSearchTerm,
18
20
  addressType,
19
21
  });
@@ -1,24 +1,34 @@
1
1
  import { type QueryOptions, useQuery } from "../../shared/hooks";
2
- import { getAddressesByUnitIdService } from "../services";
2
+ import { getAttachedAddressesByCustomerIdService } from "../services";
3
3
 
4
4
  type UseListAddressesProps = {
5
5
  orgUnitId: string;
6
+ contractId: string;
6
7
  search: string;
7
8
  page?: number;
8
9
  addressType?: string;
9
10
  };
10
11
 
11
12
  export const useListAddresses = (
12
- { orgUnitId, search, page = 1, addressType }: UseListAddressesProps,
13
- options?: QueryOptions<AwaitedType<typeof getAddressesByUnitIdService>>
13
+ {
14
+ orgUnitId,
15
+ contractId,
16
+ search,
17
+ page = 1,
18
+ addressType,
19
+ }: UseListAddressesProps,
20
+ options?: QueryOptions<
21
+ AwaitedType<typeof getAttachedAddressesByCustomerIdService>
22
+ >
14
23
  ) => {
15
24
  const { data, error, isLoading, refetch } = useQuery(
16
- `api/addresses/unitId=${orgUnitId}/page=${page}/search=${
25
+ `api/addresses/unitId=${orgUnitId}/contractId=${contractId}/page=${page}/search=${
17
26
  search ?? ""
18
27
  }/addressType=${addressType ?? ""}`,
19
28
  ({ cookie }) =>
20
- getAddressesByUnitIdService({
29
+ getAttachedAddressesByCustomerIdService({
21
30
  orgUnitId,
31
+ contractId,
22
32
  search,
23
33
  cookie,
24
34
  addressType,
@@ -90,6 +90,7 @@ export const AddressLayout = () => {
90
90
  useListAddresses(
91
91
  {
92
92
  orgUnitId: (router.query.orgUnitId as string) ?? "",
93
+ contractId: (router.query.contractId as string) ?? "",
93
94
  page: isRouterSynced ? page : 0,
94
95
  search: committedSearch,
95
96
  },
@@ -5,7 +5,7 @@ import {
5
5
  } from "../../shared/utils/withClientErrorBoundary";
6
6
  import { addressesClient } from "../clients/AddressesClient";
7
7
 
8
- export type GetAddressesByUnitIdServiceProps = Partial<{
8
+ export type GetAttachedAddressesByCustomerIdServiceProps = Partial<{
9
9
  search: string;
10
10
  orgUnitId: string;
11
11
  status: string;
@@ -14,11 +14,13 @@ export type GetAddressesByUnitIdServiceProps = Partial<{
14
14
  page?: number;
15
15
  addressType?: string;
16
16
  }> & {
17
+ contractId: string;
17
18
  cookie: string;
18
19
  };
19
20
 
20
- export const getAddressesByUnitIdServiceFn = async ({
21
+ export const getAttachedAddressesByCustomerIdServiceFn = async ({
21
22
  orgUnitId,
23
+ contractId,
22
24
  status,
23
25
  type,
24
26
  sort,
@@ -26,17 +28,19 @@ export const getAddressesByUnitIdServiceFn = async ({
26
28
  search = "",
27
29
  page = 1,
28
30
  addressType,
29
- }: GetAddressesByUnitIdServiceProps) => {
30
- if (!orgUnitId) {
31
+ }: GetAttachedAddressesByCustomerIdServiceProps) => {
32
+ if (!orgUnitId || !contractId) {
31
33
  return { data: [], total: 0 };
32
34
  }
33
- const { addresses, total } = await addressesClient.getAddressesByUnitId({
34
- orgUnitId,
35
- cookie,
36
- name: search,
37
- addressType,
38
- page,
39
- });
35
+ const { addresses, total } =
36
+ await addressesClient.getAttachedAddressesByCustomerId({
37
+ orgUnitId,
38
+ customerId: contractId,
39
+ cookie,
40
+ name: search,
41
+ addressType,
42
+ page,
43
+ });
40
44
 
41
45
  const formattedAddress = addresses
42
46
  ?.filter((address) => {
@@ -51,10 +55,10 @@ export const getAddressesByUnitIdServiceFn = async ({
51
55
  return { data: formattedAddress, total };
52
56
  };
53
57
 
54
- export const getAddressesByUnitIdService = withClientErrorBoundary(
55
- getAddressesByUnitIdServiceFn,
58
+ export const getAttachedAddressesByCustomerIdService = withClientErrorBoundary(
59
+ getAttachedAddressesByCustomerIdServiceFn,
56
60
  {
57
- componentName: "AddressesByUnitId",
61
+ componentName: "AttachedAddressesByCustomerId",
58
62
  onError: handleUnauthorizedRedirect,
59
63
  }
60
64
  );
@@ -4,9 +4,9 @@ export {
4
4
  type GetAddressesServiceProps,
5
5
  } from "./get-addresses.service";
6
6
  export {
7
- getAddressesByUnitIdService,
8
- type GetAddressesByUnitIdServiceProps,
9
- } from "./get-addresses-by-unit-id.service";
7
+ getAttachedAddressesByCustomerIdService,
8
+ type GetAttachedAddressesByCustomerIdServiceProps,
9
+ } from "./get-attached-addresses-by-customer-id.service";
10
10
  export {
11
11
  createNewAddressService,
12
12
  type CreateNewAddressServiceProps,
@@ -102,8 +102,9 @@ export type AddressInput = {
102
102
  neighborhood?: string;
103
103
  };
104
104
 
105
- export type GetAddressesByUnitIdParams = {
105
+ export type GetAttachedAddressesByCustomerIdParams = {
106
106
  orgUnitId: string;
107
+ customerId: string;
107
108
  cookie: string;
108
109
  name?: string;
109
110
  addressType?: string;
@@ -120,6 +121,7 @@ export type DefaultAddressParams = {
120
121
 
121
122
  export type UseDebouncedSearchAddressByUnitIdProps = {
122
123
  orgUnitId: string;
124
+ contractId: string;
123
125
  search: string;
124
126
  addressType?: string;
125
127
  };
@@ -14,11 +14,14 @@ import {
14
14
  VerticalNav,
15
15
  LetterHighlight,
16
16
  } from "../../../shared/components";
17
- import { useDrawerProps, useBuyerPortal } from "../../../shared/hooks";
17
+ import {
18
+ useDrawerProps,
19
+ useBuyerPortal,
20
+ useReloadOnBfcacheRestore,
21
+ } from "../../../shared/hooks";
18
22
  import { GlobalLayout } from "../../../shared/layouts";
19
23
  import { SumaSidebar } from "../../../shared/layouts/SumaPageLayout/SumaSidebar";
20
24
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
21
- import { USER_ROLES } from "../../../users/types";
22
25
  import { AddContractsDrawer } from "../../components/AddContractsDrawer/AddContractsDrawer";
23
26
  import { ConfirmRemoveDrawer } from "../../components/ConfirmRemoveDrawer/ConfirmRemoveDrawer";
24
27
  import { ContractSelectAllRow } from "../../components/ContractSelectAllRow";
@@ -32,6 +35,7 @@ import {
32
35
  useContractSwitchChannel,
33
36
  useRemoveContracts,
34
37
  useSetDefaultContract,
38
+ useSwitchContract,
35
39
  } from "../../hooks";
36
40
 
37
41
  import { buildContractSettingsNavItems } from "./ContractSettingsNav";
@@ -96,8 +100,8 @@ export const ContractListingLayout = ({
96
100
  ...addContractsDrawer
97
101
  } = useDrawerProps();
98
102
 
99
- const { featureFlags } = useBuyerPortal();
100
- const isAdmin = user?.roles?.includes(USER_ROLES.SUPER_USER) ?? false;
103
+ const { featureFlags, clientContext } = useBuyerPortal();
104
+ const { switchContract } = useSwitchContract();
101
105
 
102
106
  const {
103
107
  open: openConfirmRemoveDrawer,
@@ -119,6 +123,34 @@ export const ContractListingLayout = ({
119
123
  onSuccess: () => {
120
124
  confirmRemoveDrawer.close();
121
125
  clearSelection();
126
+
127
+ const removedActiveContract = pendingRemoveIds.has(
128
+ clientContext.customerId
129
+ );
130
+
131
+ if (removedActiveContract) {
132
+ const remainingContracts = contracts.filter(
133
+ (c) => !pendingRemoveIds.has(c.id)
134
+ );
135
+ const fallbackContractId = getDefaultContractId(remainingContracts);
136
+
137
+ if (fallbackContractId) {
138
+ switchContract(fallbackContractId, { orgUnitId: orgUnit.id }).then(
139
+ (switched) => {
140
+ if (!switched) {
141
+ pushToast({
142
+ message:
143
+ "Contracts removed, but we couldn't refresh your active contract. Please reload the page.",
144
+ status: "ERROR",
145
+ });
146
+ router.replace(router.asPath);
147
+ }
148
+ }
149
+ );
150
+ return;
151
+ }
152
+ }
153
+
122
154
  router.replace(router.asPath);
123
155
  pushToast({
124
156
  message: `${pendingRemoveIds.size} contract${
@@ -200,6 +232,8 @@ export const ContractListingLayout = ({
200
232
  return cleanup;
201
233
  }, []);
202
234
 
235
+ useReloadOnBfcacheRestore();
236
+
203
237
  useEffect(() => {
204
238
  if (router.query.error === "contract-load-failed") {
205
239
  pushToast({
@@ -289,7 +323,7 @@ export const ContractListingLayout = ({
289
323
  info={headerInfo}
290
324
  data-fs-bp-contract-listing-header
291
325
  >
292
- {featureFlags?.suma && isAdmin && (
326
+ {featureFlags?.suma && (
293
327
  <HeaderInside.Button
294
328
  onClick={openAddContractsDrawer}
295
329
  aria-label="Add contracts"
@@ -21,7 +21,7 @@
21
21
  margin: var(--fs-bp-margin-0) var(--fs-bp-margin-auto);
22
22
 
23
23
  @include u.media(">=notebook") {
24
- grid-template-columns: 16.25rem 1fr;
24
+ grid-template-columns: 20rem 1fr;
25
25
  }
26
26
  }
27
27
 
@@ -30,7 +30,7 @@
30
30
  @import "../../../shared/components/LetterHighlight/letter-highlight.scss";
31
31
 
32
32
  display: none;
33
- padding: var(--fs-bp-padding-5) var(--fs-bp-padding-5) var(--fs-bp-padding-12) var(--fs-bp-padding-2);
33
+ padding: var(--fs-bp-padding-5) var(--fs-bp-padding-5) var(--fs-bp-padding-12) var(--fs-bp-padding-5);
34
34
  border-right: 1px solid var(--fs-bp-color-neutral-2);
35
35
  overflow-y: auto;
36
36
 
@@ -104,7 +104,10 @@ export const OrgUnitsDetailsLayout = ({
104
104
  listAccountingFieldRefresh,
105
105
  } = useListAccountingFields({ orgUnitId: orgUnit.id });
106
106
 
107
- const organizationMenu = getOrganizationSettingsLinks(orgUnit.id);
107
+ const organizationMenu = getOrganizationSettingsLinks({
108
+ orgUnitId: orgUnit.id,
109
+ contractId: linkParams.contractId,
110
+ });
108
111
 
109
112
  const financeMenu = getFinanceSettingsLinks(linkParams);
110
113
 
@@ -97,7 +97,7 @@ class Client {
97
97
  ...(headers.Cookie ? getAuthFromCookie(headers.Cookie ?? "") : {}),
98
98
  ...headers,
99
99
  },
100
- ...(cache ? { cache, next: { revalidate: 300 } } : {}),
100
+ ...(cache ? { cache } : {}),
101
101
  };
102
102
 
103
103
  const paramsString = toQueryParams(params);
@@ -19,3 +19,4 @@ export { ANALYTICS_FALLBACK_CORRELATION_ID } from "./analytics/constants";
19
19
  export { useGetScopeConfig, SCOPE_KEYS } from "./useGetScopeConfig";
20
20
  export { useSetScopeConfig } from "./useSetScopeConfig";
21
21
  export { useUrlPaginatedSearch } from "./useUrlPaginatedSearch";
22
+ export { useReloadOnBfcacheRestore } from "./useReloadOnBfcacheRestore";
@@ -0,0 +1,24 @@
1
+ import { useEffect } from "react";
2
+
3
+ import { useRouter } from "next/router";
4
+
5
+ /**
6
+ * Forces a full reload when the page is restored from the browser's
7
+ * back-forward cache (e.g. after pressing Back), so contract-scoped layouts
8
+ * never resume with frozen JS state/closures from before a prior contract
9
+ * switch.
10
+ */
11
+ export const useReloadOnBfcacheRestore = () => {
12
+ const router = useRouter();
13
+
14
+ useEffect(() => {
15
+ const handlePageShow = (event: PageTransitionEvent) => {
16
+ if (event.persisted) {
17
+ router.reload();
18
+ }
19
+ };
20
+
21
+ window.addEventListener("pageshow", handlePageShow);
22
+ return () => window.removeEventListener("pageshow", handlePageShow);
23
+ }, []);
24
+ };
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
3
3
  import { useRouter } from "next/router";
4
4
 
5
5
  import { useBuyerPortal } from "../../hooks";
6
+ import { resolveContractId } from "../../utils";
6
7
  import { FullSidebarNav } from "../SumaPageLayout/FullSidebarNav";
7
8
 
8
9
  export type SidebarMenuProps = {
@@ -16,21 +17,19 @@ export type SidebarMenuProps = {
16
17
  loading?: boolean;
17
18
  };
18
19
 
19
- export const SidebarMenu = ({ ...otherProps }: SidebarMenuProps) => {
20
+ export const SidebarMenu = ({ loading, ...otherProps }: SidebarMenuProps) => {
20
21
  const { currentOrgUnit, currentContract } = useBuyerPortal();
21
22
  const router = useRouter();
22
23
 
23
24
  const orgUnitId = (router.query.orgUnitId as string) || "";
24
- const customerId = (router.query.contractId as string) || "";
25
-
26
- const idContract =
27
- (currentContract?.id
28
- ? currentContract?.id
29
- : currentOrgUnit?.customerGroup.customerIds[0]) ?? "";
30
25
 
31
26
  const linkParams = {
32
27
  orgUnitId: orgUnitId || currentOrgUnit?.id || "",
33
- contractId: customerId || idContract,
28
+ contractId: resolveContractId({
29
+ queryContractId: router.query.contractId,
30
+ currentContract,
31
+ currentOrgUnit,
32
+ }),
34
33
  };
35
34
 
36
35
  return (
@@ -38,6 +37,7 @@ export const SidebarMenu = ({ ...otherProps }: SidebarMenuProps) => {
38
37
  <FullSidebarNav
39
38
  orgUnitId={linkParams.orgUnitId}
40
39
  contractId={linkParams.contractId}
40
+ loading={loading}
41
41
  />
42
42
  </aside>
43
43
  );
@@ -4,7 +4,7 @@ import { DropdownItem } from "@faststore/ui";
4
4
 
5
5
  import { OrgUnitDetailsNavbar } from "../../../org-units/components";
6
6
  import { Icon } from "../../components";
7
- import { useBuyerPortal } from "../../hooks";
7
+ import { useBuyerPortal, useReloadOnBfcacheRestore } from "../../hooks";
8
8
  import { getContractSettingsLinks } from "../../utils";
9
9
  import { BaseTabsLayout } from "../BaseTabsLayout/BaseTabsLayout";
10
10
  import { SumaSidebar } from "../SumaPageLayout/SumaSidebar";
@@ -29,6 +29,8 @@ export const ContractTabsLayout = ({
29
29
  const { currentOrgUnit, currentUser, currentContract, featureFlags } =
30
30
  useBuyerPortal();
31
31
 
32
+ useReloadOnBfcacheRestore();
33
+
32
34
  const resolvedOrgUnitId = currentOrgUnit?.id ?? orgUnitId ?? "";
33
35
  const resolvedContractId =
34
36
  currentContract?.id ??
@@ -57,6 +59,7 @@ export const ContractTabsLayout = ({
57
59
  contractId={resolvedContractId}
58
60
  variant="full"
59
61
  showBackLink
62
+ loading={loading}
60
63
  />
61
64
  <section data-fs-bp-contract-listing-section>{children}</section>
62
65
  </div>
@@ -17,7 +17,10 @@ export const OrgUnitTabsLayout = ({
17
17
  }: OrgUnitTabsLayoutProps) => {
18
18
  const { currentOrgUnit, currentUser, currentContract } = useBuyerPortal();
19
19
 
20
- const verticalLinks = getOrganizationSettingsLinks(currentOrgUnit?.id ?? "");
20
+ const verticalLinks = getOrganizationSettingsLinks({
21
+ orgUnitId: currentOrgUnit?.id ?? "",
22
+ contractId: currentContract?.id ?? "",
23
+ });
21
24
 
22
25
  return (
23
26
  <BaseTabsLayout data-fs-bp-org-unit-tabs-layout>
@@ -12,6 +12,7 @@ import {
12
12
  getContractSettingsLinks,
13
13
  getFinanceSettingsLinks,
14
14
  getOrganizationSettingsLinks,
15
+ resolveContractId,
15
16
  } from "../../utils";
16
17
  import { buyerPortalRoutes } from "../../utils/buyerPortalRoutes";
17
18
 
@@ -21,6 +22,7 @@ export type FullSidebarNavProps = {
21
22
  orgUnitId: string;
22
23
  contractId: string;
23
24
  showContractTitle?: boolean;
25
+ loading?: boolean;
24
26
  };
25
27
 
26
28
  const mapLinksHandler = ({
@@ -35,6 +37,7 @@ export const FullSidebarNav = ({
35
37
  orgUnitId,
36
38
  contractId,
37
39
  showContractTitle = true,
40
+ loading = false,
38
41
  }: FullSidebarNavProps) => {
39
42
  const { currentOrgUnit, currentContract } = useBuyerPortal();
40
43
 
@@ -46,21 +49,26 @@ export const FullSidebarNav = ({
46
49
 
47
50
  const resolvedOrgUnitId = orgUnitId || currentOrgUnit?.id || "";
48
51
  const resolvedContractId =
49
- contractId ||
50
- currentContract?.id ||
51
- currentOrgUnit?.customerGroup.customerIds[0] ||
52
- "";
52
+ contractId || resolveContractId({ currentContract, currentOrgUnit });
53
53
 
54
54
  const linkParams = {
55
55
  orgUnitId: resolvedOrgUnitId,
56
56
  contractId: resolvedContractId,
57
57
  };
58
58
 
59
+ // Skip the fetch entirely while this sidebar is just a transient loading
60
+ // placeholder (e.g. LoadingTabsLayout during a route transition) — it would
61
+ // otherwise fire with route-parsed, possibly-stale ids. The item below still
62
+ // renders (just with no children yet), since data/loading naturally stay at
63
+ // their initial empty/false values while the fetch is skipped.
59
64
  const {
60
65
  listAccountingFieldLoading,
61
66
  listAccountingFieldData,
62
67
  listAccountingFieldRefresh,
63
- } = useListAccountingFields({ orgUnitId: resolvedOrgUnitId });
68
+ } = useListAccountingFields(
69
+ { orgUnitId: resolvedOrgUnitId },
70
+ { lazy: loading }
71
+ );
64
72
 
65
73
  const contractMenu =
66
74
  getContractSettingsLinks(linkParams).map(mapLinksHandler);
@@ -95,7 +103,10 @@ export const FullSidebarNav = ({
95
103
  ),
96
104
  });
97
105
 
98
- const organizationMenu = getOrganizationSettingsLinks(resolvedOrgUnitId);
106
+ const organizationMenu = getOrganizationSettingsLinks({
107
+ orgUnitId: resolvedOrgUnitId,
108
+ contractId: resolvedContractId,
109
+ });
99
110
  const financeMenu = getFinanceSettingsLinks(linkParams);
100
111
 
101
112
  return (
@@ -1,7 +1,10 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
+ import { useRouter } from "next/router";
4
+
3
5
  import { OrgUnitDetailsNavbar } from "../../../org-units/components";
4
- import { useBuyerPortal } from "../../hooks";
6
+ import { useBuyerPortal, useReloadOnBfcacheRestore } from "../../hooks";
7
+ import { resolveContractId } from "../../utils";
5
8
 
6
9
  import { SumaSidebar } from "./SumaSidebar";
7
10
 
@@ -20,13 +23,19 @@ export const SumaPageLayout = ({
20
23
  }: SumaPageLayoutProps) => {
21
24
  const { featureFlags, currentOrgUnit, currentUser, currentContract } =
22
25
  useBuyerPortal();
26
+ const router = useRouter();
27
+
28
+ useReloadOnBfcacheRestore();
23
29
 
24
30
  if (!featureFlags?.suma || !currentOrgUnit?.id) {
25
31
  return <>{children}</>;
26
32
  }
27
33
 
28
- const contractId =
29
- currentContract?.id ?? currentOrgUnit.customerGroup.customerIds[0] ?? "";
34
+ const contractId = resolveContractId({
35
+ queryContractId: router.query.contractId,
36
+ currentContract,
37
+ currentOrgUnit,
38
+ });
30
39
 
31
40
  return (
32
41
  <>
@@ -48,6 +57,7 @@ export const SumaPageLayout = ({
48
57
  contractId={contractId}
49
58
  variant={sidebarVariant}
50
59
  showBackLink={showBackLink}
60
+ loading={loading}
51
61
  />
52
62
  <section data-fs-bp-contract-listing-section>{children}</section>
53
63
  </div>
@@ -12,6 +12,7 @@ export type SumaSidebarProps = {
12
12
  contractId: string;
13
13
  variant?: "listing" | "full";
14
14
  showBackLink?: boolean;
15
+ loading?: boolean;
15
16
  };
16
17
 
17
18
  export const SumaSidebar = ({
@@ -19,6 +20,7 @@ export const SumaSidebar = ({
19
20
  contractId,
20
21
  variant = "full",
21
22
  showBackLink = false,
23
+ loading = false,
22
24
  }: SumaSidebarProps) => {
23
25
  const { featureFlags, currentContract } = useBuyerPortal();
24
26
  const contractsUrl = buyerPortalRoutes.contracts({ orgUnitId });
@@ -73,6 +75,7 @@ export const SumaSidebar = ({
73
75
  orgUnitId={orgUnitId}
74
76
  contractId={contractId}
75
77
  showContractTitle={!showIdentity}
78
+ loading={loading}
76
79
  />
77
80
  )}
78
81
  </aside>
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { resolveContractId } from "../resolveContractId";
4
+
5
+ import type { ContractData } from "../../../contracts/types";
6
+ import type { OrgUnitBasicData } from "../../../org-units/types";
7
+
8
+ const contract = { id: "contract-b" } as ContractData;
9
+ const orgUnit = {
10
+ customerGroup: { customerIds: ["contract-a", "contract-b"] },
11
+ } as OrgUnitBasicData;
12
+
13
+ describe("resolveContractId", () => {
14
+ it("prefers the URL's contractId over context", () => {
15
+ expect(
16
+ resolveContractId({
17
+ queryContractId: "contract-c",
18
+ currentContract: contract,
19
+ currentOrgUnit: orgUnit,
20
+ })
21
+ ).toBe("contract-c");
22
+ });
23
+
24
+ it("takes the first value when the URL param is an array", () => {
25
+ expect(
26
+ resolveContractId({
27
+ queryContractId: ["contract-c", "contract-d"],
28
+ currentContract: contract,
29
+ currentOrgUnit: orgUnit,
30
+ })
31
+ ).toBe("contract-c");
32
+ });
33
+
34
+ it("falls back to currentContract when the URL has no contractId", () => {
35
+ expect(
36
+ resolveContractId({ currentContract: contract, currentOrgUnit: orgUnit })
37
+ ).toBe("contract-b");
38
+ });
39
+
40
+ it("falls back to the org unit's first customer id as a last resort", () => {
41
+ expect(resolveContractId({ currentOrgUnit: orgUnit })).toBe("contract-a");
42
+ });
43
+
44
+ it("returns an empty string when nothing is known", () => {
45
+ expect(resolveContractId({})).toBe("");
46
+ });
47
+ });
@@ -10,6 +10,12 @@ function replaceParams(template: string, params: Record<string, string>) {
10
10
  .replace(/\/+/g, "/"); // Remove consecutive slashes
11
11
  }
12
12
 
13
+ function appendContractIdParam(url: string, contractId?: string): string {
14
+ return contractId
15
+ ? `${url}?contractId=${encodeURIComponent(contractId)}`
16
+ : url;
17
+ }
18
+
13
19
  export const buyerPortalRoutes = {
14
20
  // orgUnitId + contractId
15
21
  profileDetails: (params: { orgUnitId: string; contractId: string }) =>
@@ -66,18 +72,34 @@ export const buyerPortalRoutes = {
66
72
  `${base}/buying-policy/[orgUnitId]/[contractId]/[buyingPolicyId]`,
67
73
  params
68
74
  ),
69
- // orgUnitId only
70
- users: (params: { orgUnitId: string }) =>
71
- replaceParams(`${base}/users/[orgUnitId]`, params),
75
+ // orgUnitId only (contractId is optional and only carried in the URL so
76
+ // the sidebar can keep linking to the right contract elsewhere)
77
+ users: (params: { orgUnitId: string; contractId?: string }) =>
78
+ appendContractIdParam(
79
+ replaceParams(`${base}/users/[orgUnitId]`, {
80
+ orgUnitId: params.orgUnitId,
81
+ }),
82
+ params.contractId
83
+ ),
72
84
 
73
- roles: (params: { orgUnitId: string }) =>
74
- replaceParams(`${base}/roles/[orgUnitId]`, params),
85
+ roles: (params: { orgUnitId: string; contractId?: string }) =>
86
+ appendContractIdParam(
87
+ replaceParams(`${base}/roles/[orgUnitId]`, {
88
+ orgUnitId: params.orgUnitId,
89
+ }),
90
+ params.contractId
91
+ ),
75
92
 
76
93
  roleDetails: (params: { orgUnitId: string; roleName: string }) =>
77
94
  replaceParams(`${base}/roles/[orgUnitId]/[roleName]`, params),
78
95
 
79
- orgUnits: (params: { orgUnitId: string }) =>
80
- replaceParams(`${base}/org-units/[orgUnitId]`, params),
96
+ orgUnits: (params: { orgUnitId: string; contractId?: string }) =>
97
+ appendContractIdParam(
98
+ replaceParams(`${base}/org-units/[orgUnitId]`, {
99
+ orgUnitId: params.orgUnitId,
100
+ }),
101
+ params.contractId
102
+ ),
81
103
 
82
104
  // orgUnitId + specific ID
83
105
  userDetails: (params: { orgUnitId: string; userId: string }) =>
@@ -22,7 +22,7 @@ export const SCOPE_KEYS = {
22
22
  CREDIT_CARDS: "creditCards",
23
23
  } as const;
24
24
 
25
- export const CURRENT_VERSION = "2.0.10";
25
+ export const CURRENT_VERSION = "2.0.11";
26
26
 
27
27
  export const CHANGES_TIMEOUT_MESSAGE =
28
28
  "Changes may take up to 10 minutes to apply.";
@@ -1,10 +1,16 @@
1
1
  import { buyerPortalRoutes } from "./buyerPortalRoutes";
2
2
 
3
- export const getOrganizationSettingsLinks = (orgUnitId: string) => [
4
- { name: "Users", link: buyerPortalRoutes.users({ orgUnitId }) },
5
- { name: "Roles", link: buyerPortalRoutes.roles({ orgUnitId }) },
3
+ export const getOrganizationSettingsLinks = ({
4
+ orgUnitId,
5
+ contractId,
6
+ }: {
7
+ orgUnitId: string;
8
+ contractId?: string;
9
+ }) => [
10
+ { name: "Users", link: buyerPortalRoutes.users({ orgUnitId, contractId }) },
11
+ { name: "Roles", link: buyerPortalRoutes.roles({ orgUnitId, contractId }) },
6
12
  {
7
13
  name: "Organizational Units",
8
- link: buyerPortalRoutes.orgUnits({ orgUnitId }),
14
+ link: buyerPortalRoutes.orgUnits({ orgUnitId, contractId }),
9
15
  },
10
16
  ];
@@ -13,6 +13,6 @@ export const getSumaListingSidebarLinks = ({
13
13
  name: "Contracts",
14
14
  link: buyerPortalRoutes.contracts({ orgUnitId }),
15
15
  },
16
- ...getOrganizationSettingsLinks(orgUnitId),
16
+ ...getOrganizationSettingsLinks({ orgUnitId, contractId }),
17
17
  ...getFinanceSettingsLinks({ orgUnitId, contractId }),
18
18
  ];
@@ -23,6 +23,7 @@ export {
23
23
  } from "./getKeyByValue";
24
24
  export { getOrganizationSettingsLinks } from "./getOrganizationSettingsLinks";
25
25
  export { getSumaListingSidebarLinks } from "./getSumaListingSidebarLinks";
26
+ export { resolveContractId } from "./resolveContractId";
26
27
  export { getTreeDepth } from "./getTreeDepth";
27
28
  export { doLogout } from "./logout";
28
29
  export { transformToDecimalMoney } from "./monetaryConverter";
@@ -0,0 +1,30 @@
1
+ import type { ContractData } from "../../contracts/types";
2
+ import type { OrgUnitBasicData } from "../../org-units/types";
3
+
4
+ export type ResolveContractIdParams = {
5
+ queryContractId?: string | string[];
6
+ currentContract?: ContractData | null;
7
+ currentOrgUnit?: OrgUnitBasicData | null;
8
+ };
9
+
10
+ /**
11
+ * Single source of truth for which contract the sidebar should link to.
12
+ * The URL's `contractId` (when present) always wins; `customerIds[0]` is a
13
+ * last resort for direct/bookmarked visits where nothing else is known.
14
+ */
15
+ export const resolveContractId = ({
16
+ queryContractId,
17
+ currentContract,
18
+ currentOrgUnit,
19
+ }: ResolveContractIdParams): string => {
20
+ const fromQuery = Array.isArray(queryContractId)
21
+ ? queryContractId[0]
22
+ : queryContractId;
23
+
24
+ return (
25
+ fromQuery ||
26
+ currentContract?.id ||
27
+ currentOrgUnit?.customerGroup.customerIds[0] ||
28
+ ""
29
+ );
30
+ };
@@ -3,6 +3,7 @@ import { getBuyerPortalFeatureFlags } from "../services/feature-flags/get-featur
3
3
 
4
4
  import { ClientContext, getClientContext } from "./getClientContext";
5
5
 
6
+ import type { FeatureFlags } from "../components/BuyerPortalProvider/BuyerPortalProvider";
6
7
  import type { AuthRouteProps } from "../types";
7
8
  import type { LoaderData } from "../types";
8
9
 
@@ -10,13 +11,18 @@ export type WithAuthLoaderOptions = {
10
11
  validateAccess?: (props: { cookie: string }) => Promise<boolean>;
11
12
  };
12
13
 
14
+ export type WithAuthLoaderClientContext = ClientContext & {
15
+ /** Single in-flight fetch shared with the flags merge below — reuse this instead of calling getBuyerPortalFeatureFlags again. */
16
+ featureFlagsPromise: Promise<FeatureFlags>;
17
+ };
18
+
13
19
  /**
14
20
  * Utility function to encapsulate access validation in loaders.
15
21
  * Similar to withAuthProvider but for server-side usage.
16
22
  */
17
23
  export const withAuthLoader = async <T>(
18
24
  data: LoaderData,
19
- dataLoader: (clientContext: ClientContext) => Promise<T>,
25
+ dataLoader: (clientContext: WithAuthLoaderClientContext) => Promise<T>,
20
26
  options?: WithAuthLoaderOptions
21
27
  ): Promise<AuthRouteProps<T>> => {
22
28
  try {
@@ -35,9 +41,11 @@ export const withAuthLoader = async <T>(
35
41
  };
36
42
  }
37
43
 
44
+ const featureFlagsPromise = getBuyerPortalFeatureFlags(cookie);
45
+
38
46
  const [pageData, featureFlags] = await Promise.all([
39
- dataLoader({ cookie, userId, ...clientContext }),
40
- getBuyerPortalFeatureFlags(cookie),
47
+ dataLoader({ cookie, userId, ...clientContext, featureFlagsPromise }),
48
+ featureFlagsPromise,
41
49
  ]);
42
50
 
43
51
  const mergedPageData = (() => {
@@ -3,7 +3,6 @@ import { listAttachedContractsService } from "../features/contracts/services";
3
3
  import { getOrgUnitBasicDataService } from "../features/org-units/services";
4
4
  import { withErrorBoundary } from "../features/shared/components";
5
5
  import { ErrorTabsLayout } from "../features/shared/layouts/ErrorTabsLayout/ErrorTabsLayout";
6
- import { getBuyerPortalFeatureFlags } from "../features/shared/services/feature-flags/get-feature-flags.service";
7
6
  import {
8
7
  type ClientContext,
9
8
  withLoaderErrorBoundary,
@@ -46,35 +45,38 @@ const loaderFunction = async (
46
45
  throw new Error(`Missing required query param: orgUnitId=${orgUnitId}`);
47
46
  }
48
47
 
49
- return withAuthLoader(data, async ({ cookie, userId, ...clientContext }) => {
50
- const featureFlags = await getBuyerPortalFeatureFlags(cookie);
48
+ return withAuthLoader(
49
+ data,
50
+ async ({ cookie, userId, featureFlagsPromise, ...clientContext }) => {
51
+ const featureFlags = await featureFlagsPromise;
51
52
 
52
- if (!featureFlags?.suma) {
53
- data.res?.writeHead(302, {
54
- Location: buyerPortalRoutes.orgUnitDetails({ orgUnitId }),
55
- });
56
- data.res?.end();
53
+ if (!featureFlags?.suma) {
54
+ data.res?.writeHead(302, {
55
+ Location: buyerPortalRoutes.orgUnitDetails({ orgUnitId }),
56
+ });
57
+ data.res?.end();
57
58
 
58
- return { redirected: true } as unknown as ContractsPageData;
59
- }
59
+ return { redirected: true } as unknown as ContractsPageData;
60
+ }
60
61
 
61
- const [orgUnit, contracts, user] = await Promise.all([
62
- getOrgUnitBasicDataService({ id: orgUnitId, cookie }),
63
- listAttachedContractsService({ orgUnitId, cookie }),
64
- getUserByIdService({ orgUnitId, userId, cookie }),
65
- ]);
62
+ const [orgUnit, contracts, user] = await Promise.all([
63
+ getOrgUnitBasicDataService({ id: orgUnitId, cookie }),
64
+ listAttachedContractsService({ orgUnitId, cookie }),
65
+ getUserByIdService({ orgUnitId, userId, cookie }),
66
+ ]);
66
67
 
67
- return {
68
- data: {
69
- orgUnit,
70
- contracts: contracts ?? [],
71
- user,
72
- },
73
- context: {
74
- clientContext: { cookie, userId, ...clientContext },
75
- },
76
- };
77
- });
68
+ return {
69
+ data: {
70
+ orgUnit,
71
+ contracts: contracts ?? [],
72
+ user,
73
+ },
74
+ context: {
75
+ clientContext: { cookie, userId, ...clientContext },
76
+ },
77
+ };
78
+ }
79
+ );
78
80
  };
79
81
 
80
82
  export const loader = withLoaderErrorBoundary(loaderFunction, {
@@ -1,3 +1,4 @@
1
+ import { getContractDetailsService } from "../features/contracts/services";
1
2
  import { OrgUnitsLayout } from "../features/org-units/layouts";
2
3
  import {
3
4
  getChildrenOrgUnitsService,
@@ -16,6 +17,7 @@ import {
16
17
  } from "../features/shared/utils";
17
18
  import { getUserByIdService } from "../features/users/services";
18
19
 
20
+ import type { ContractData } from "../features/contracts/types";
19
21
  import type {
20
22
  OrgUnitBasicData,
21
23
  OrgUnitSearchResponse,
@@ -29,6 +31,7 @@ export type OrgUnitsPageData = {
29
31
  context: {
30
32
  clientContext: ClientContext;
31
33
  currentOrgUnit: OrgUnitBasicData | null;
34
+ currentContract: ContractData | null;
32
35
  currentUser: UserData | null;
33
36
  };
34
37
  hasError?: boolean;
@@ -37,13 +40,14 @@ export type OrgUnitsPageData = {
37
40
 
38
41
  type OrgUnitsPageQuery = {
39
42
  orgUnitId: string;
43
+ contractId?: string;
40
44
  search?: string;
41
45
  };
42
46
 
43
47
  const loaderFunction = async (
44
48
  data: LoaderData<OrgUnitsPageQuery>
45
49
  ): Promise<AuthRouteProps<OrgUnitsPageData>> => {
46
- const { search, orgUnitId } = data.query;
50
+ const { search, orgUnitId, contractId } = data.query;
47
51
 
48
52
  return withAuthLoader(
49
53
  data,
@@ -55,6 +59,18 @@ const loaderFunction = async (
55
59
 
56
60
  const user = await getUserByIdService({ orgUnitId, userId, cookie });
57
61
 
62
+ // Organizational Units don't need contract data, but the sidebar
63
+ // (rendered by shared layouts) uses `currentContract` for its identity
64
+ // block, so fetch it when the URL carries the contract the user was
65
+ // last viewing.
66
+ const currentContract = contractId
67
+ ? await getContractDetailsService({
68
+ contractId,
69
+ unitId: orgUnitId,
70
+ cookie,
71
+ })
72
+ : null;
73
+
58
74
  const clientContext = {
59
75
  cookie,
60
76
  customerId,
@@ -73,6 +89,7 @@ const loaderFunction = async (
73
89
  context: {
74
90
  clientContext,
75
91
  currentOrgUnit,
92
+ currentContract,
76
93
  currentUser: user,
77
94
  },
78
95
  };
@@ -99,6 +116,7 @@ const loaderFunction = async (
99
116
  context: {
100
117
  clientContext,
101
118
  currentOrgUnit,
119
+ currentContract,
102
120
  currentUser: user,
103
121
  },
104
122
  };
@@ -113,6 +131,7 @@ const loaderFunction = async (
113
131
  context: {
114
132
  clientContext,
115
133
  currentOrgUnit,
134
+ currentContract,
116
135
  currentUser: user,
117
136
  },
118
137
  };
@@ -1,3 +1,4 @@
1
+ import { getContractDetailsService } from "../features/contracts/services";
1
2
  import { getOrgUnitBasicDataService } from "../features/org-units/services";
2
3
  import { RolesLayout } from "../features/roles/layout";
3
4
  import { getRolesAndPermissionsService } from "../features/roles/services";
@@ -13,6 +14,7 @@ import {
13
14
  } from "../features/shared/utils";
14
15
  import { getUserByIdService } from "../features/users/services";
15
16
 
17
+ import type { ContractData } from "../features/contracts/types";
16
18
  import type { OrgUnitBasicData } from "../features/org-units/types";
17
19
  import type { AuthRouteProps, LoaderData } from "../features/shared/types";
18
20
  import type { UserData } from "../features/users/types";
@@ -24,30 +26,45 @@ export type RolesPageData = {
24
26
  context: {
25
27
  clientContext: ClientContext;
26
28
  currentOrgUnit: OrgUnitBasicData | null;
29
+ currentContract: ContractData | null;
27
30
  currentUser: UserData | null;
28
31
  };
29
32
  hasError?: boolean;
30
33
  error?: ErrorBoundaryProps;
31
34
  };
32
35
 
36
+ export type RolesPageQuery = {
37
+ orgUnitId: string;
38
+ contractId?: string;
39
+ };
40
+
33
41
  const loaderFunction = async (
34
- data: LoaderData
42
+ data: LoaderData<RolesPageQuery>
35
43
  ): Promise<AuthRouteProps<RolesPageData>> => {
36
- const { orgUnitId } = data.query;
44
+ const { orgUnitId, contractId } = data.query;
37
45
 
38
46
  return withAuthLoader(data, async ({ cookie, userId, ...clientContext }) => {
39
- const [rolesAndPermissions, currentOrgUnit, user] = await Promise.all([
40
- getRolesAndPermissionsService({
41
- unitId: orgUnitId,
42
- customerId: clientContext.customerId,
43
- cookie,
44
- }),
45
- getOrgUnitBasicDataService({
46
- id: orgUnitId,
47
- cookie,
48
- }),
49
- getUserByIdService({ orgUnitId, userId, cookie }),
50
- ]);
47
+ // Roles don't need contract data, but the sidebar (rendered by shared
48
+ // layouts) uses `currentContract` for its identity block, so fetch it
49
+ // when the URL carries the contract the user was last viewing.
50
+ const contractDetailsPromise = contractId
51
+ ? getContractDetailsService({ contractId, unitId: orgUnitId, cookie })
52
+ : Promise.resolve(null);
53
+
54
+ const [rolesAndPermissions, currentOrgUnit, user, currentContract] =
55
+ await Promise.all([
56
+ getRolesAndPermissionsService({
57
+ unitId: orgUnitId,
58
+ customerId: clientContext.customerId,
59
+ cookie,
60
+ }),
61
+ getOrgUnitBasicDataService({
62
+ id: orgUnitId,
63
+ cookie,
64
+ }),
65
+ getUserByIdService({ orgUnitId, userId, cookie }),
66
+ contractDetailsPromise,
67
+ ]);
51
68
 
52
69
  return {
53
70
  data: {
@@ -56,6 +73,7 @@ const loaderFunction = async (
56
73
  context: {
57
74
  clientContext: { cookie, userId, ...clientContext },
58
75
  currentOrgUnit,
76
+ currentContract,
59
77
  currentUser: user,
60
78
  },
61
79
  };
@@ -1,3 +1,4 @@
1
+ import { getContractDetailsService } from "../features/contracts/services";
1
2
  import { getOrgUnitBasicDataService } from "../features/org-units/services";
2
3
  import { getRolesIdsService } from "../features/roles/services";
3
4
  import { RoleData } from "../features/roles/types";
@@ -17,6 +18,7 @@ import {
17
18
  getUsersByOrgUnitIdService,
18
19
  } from "../features/users/services";
19
20
 
21
+ import type { ContractData } from "../features/contracts/types";
20
22
  import type { OrgUnitBasicData } from "../features/org-units/types";
21
23
  import type { AuthRouteProps, LoaderData } from "../features/shared/types";
22
24
  import type {
@@ -35,6 +37,7 @@ export type UsersPageData = {
35
37
  context: {
36
38
  clientContext: ClientContext;
37
39
  currentOrgUnit: OrgUnitBasicData | null;
40
+ currentContract: ContractData | null;
38
41
  currentUser: UserData | null;
39
42
  rolesOptions: RoleData[];
40
43
  };
@@ -44,6 +47,7 @@ export type UsersPageData = {
44
47
 
45
48
  export type UsersPageQuery = GetUsersByOrgUnitIdServiceProps & {
46
49
  orgUnitId: string;
50
+ contractId?: string;
47
51
  search?: string;
48
52
  page?: string;
49
53
  };
@@ -51,7 +55,7 @@ export type UsersPageQuery = GetUsersByOrgUnitIdServiceProps & {
51
55
  const loaderFunction = async (
52
56
  data: LoaderData<UsersPageQuery>
53
57
  ): Promise<AuthRouteProps<UsersPageData>> => {
54
- const { orgUnitId, search = "", page = "1" } = data.query;
58
+ const { orgUnitId, contractId, search = "", page = "1" } = data.query;
55
59
 
56
60
  return withAuthLoader(
57
61
  data,
@@ -71,30 +75,48 @@ const loaderFunction = async (
71
75
  clientContext: { cookie, userId, customerId, ...clientContext },
72
76
  currentUser: null,
73
77
  currentOrgUnit: null,
78
+ currentContract: null,
74
79
  rolesOptions: [],
75
80
  },
76
81
  };
77
82
  }
78
83
 
79
- const [currentOrgUnit, usersResponse, user, rolesOptions] =
80
- await Promise.all([
81
- getOrgUnitBasicDataService({
82
- id: orgUnitId,
83
- cookie,
84
- }),
85
- getUsersByOrgUnitIdService({
86
- cookie,
87
- orgUnitId,
88
- search: search ?? "",
89
- page: validPage ?? 1,
90
- }),
91
- getUserByIdService({ orgUnitId, userId, cookie }),
92
- getRolesIdsService({
84
+ // Users don't need contract data, but the sidebar (rendered by shared
85
+ // layouts) uses `currentContract` for its identity block, so fetch it
86
+ // when the URL carries the contract the user was last viewing.
87
+ const contractDetailsPromise = contractId
88
+ ? getContractDetailsService({
89
+ contractId,
93
90
  unitId: orgUnitId,
94
91
  cookie,
95
- customerId,
96
- }),
97
- ]);
92
+ })
93
+ : Promise.resolve(null);
94
+
95
+ const [
96
+ currentOrgUnit,
97
+ usersResponse,
98
+ user,
99
+ rolesOptions,
100
+ currentContract,
101
+ ] = await Promise.all([
102
+ getOrgUnitBasicDataService({
103
+ id: orgUnitId,
104
+ cookie,
105
+ }),
106
+ getUsersByOrgUnitIdService({
107
+ cookie,
108
+ orgUnitId,
109
+ search: search ?? "",
110
+ page: validPage ?? 1,
111
+ }),
112
+ getUserByIdService({ orgUnitId, userId, cookie }),
113
+ getRolesIdsService({
114
+ unitId: orgUnitId,
115
+ cookie,
116
+ customerId,
117
+ }),
118
+ contractDetailsPromise,
119
+ ]);
98
120
 
99
121
  const { users, total } = usersResponse;
100
122
 
@@ -116,6 +138,7 @@ const loaderFunction = async (
116
138
  context: {
117
139
  clientContext: { cookie, userId, customerId, ...clientContext },
118
140
  currentOrgUnit,
141
+ currentContract,
119
142
  currentUser: user,
120
143
  rolesOptions,
121
144
  },