@vtex/faststore-plugin-buyer-portal 2.0.8 → 2.0.9
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/.github/workflows/release.yaml +4 -4
- package/AGENTS.md +6 -0
- package/CHANGELOG.md +23 -1
- package/docs/superpowers/plans/2026-06-26-contracts-listing-selection-search-pagination.md +1070 -0
- package/docs/superpowers/specs/2026-06-26-contracts-listing-selection-search-pagination-design.md +157 -0
- package/package.json +1 -1
- package/plugin.config.js +8 -0
- package/public/buyer-portal-icons.svg +37 -1
- package/specs/001-suma-phase-2/checklists/requirements.md +36 -0
- package/specs/001-suma-phase-2/contracts/contracts-api.md +187 -0
- package/specs/001-suma-phase-2/contracts/feature-flag.md +91 -0
- package/specs/001-suma-phase-2/data-model.md +119 -0
- package/specs/001-suma-phase-2/plan.md +123 -0
- package/specs/001-suma-phase-2/quickstart.md +156 -0
- package/specs/001-suma-phase-2/research.md +150 -0
- package/specs/001-suma-phase-2/spec.md +186 -0
- package/specs/001-suma-phase-2/tasks.md +253 -0
- package/specs/suma-add-contracts.md +224 -0
- package/specs/suma-contract-listing.md +295 -0
- package/specs/suma-contract-switching.md +201 -0
- package/specs/suma-remove-contracts.md +233 -0
- package/specs/suma-set-default-contract.md +208 -0
- package/src/features/budgets/layouts/BudgetsLayout/BudgetsLayout.tsx +113 -105
- package/src/features/buying-policies/layouts/BuyingPoliciesLayout/BuyingPoliciesLayout.tsx +60 -46
- package/src/features/contracts/clients/ContractsClient.ts +45 -0
- package/src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx +323 -0
- package/src/features/contracts/components/AddContractsDrawer/add-contracts-drawer.scss +282 -0
- package/src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx +58 -0
- package/src/features/contracts/components/ConfirmRemoveDrawer/confirm-remove-drawer.scss +18 -0
- package/src/features/contracts/components/ContractListItem/ContractListItem.tsx +120 -0
- package/src/features/contracts/components/ContractSelectAllRow/ContractSelectAllRow.tsx +37 -0
- package/src/features/contracts/components/ContractSelectAllRow/index.ts +2 -0
- package/src/features/contracts/components/ContractSelectionBar/ContractSelectionBar.tsx +47 -0
- package/src/features/contracts/components/ContractSelectionBar/index.ts +2 -0
- package/src/features/contracts/components/ContractSelectionList/ContractSelectionList.tsx +55 -0
- package/src/features/contracts/components/ContractSwitchOverlay/ContractSwitchOverlay.tsx +36 -0
- package/src/features/contracts/components/ContractSwitchOverlay/contract-switch-overlay.scss +7 -0
- package/src/features/contracts/components/ContractsListHeader/ContractsListHeader.tsx +58 -0
- package/src/features/contracts/components/ContractsListHeader/index.ts +2 -0
- package/src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx +69 -0
- package/src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx +52 -0
- package/src/features/contracts/components/UnableToRemoveContractsDrawer/unable-to-remove-contracts-drawer.scss +29 -0
- package/src/features/contracts/components/index.ts +14 -0
- package/src/features/contracts/hooks/__tests__/contractListingHelpers.test.ts +71 -0
- package/src/features/contracts/hooks/__tests__/contractSwitchChannelHelpers.test.ts +32 -0
- package/src/features/contracts/hooks/__tests__/useAddContracts.test.ts +63 -0
- package/src/features/contracts/hooks/__tests__/useContractSelection.test.ts +72 -0
- package/src/features/contracts/hooks/__tests__/useRemoveContracts.test.ts +130 -0
- package/src/features/contracts/hooks/__tests__/useSetDefaultContract.test.ts +82 -0
- package/src/features/contracts/hooks/contractListingHelpers.ts +45 -0
- package/src/features/contracts/hooks/contractSelectionHelpers.ts +28 -0
- package/src/features/contracts/hooks/contractSwitchChannelHelpers.ts +9 -0
- package/src/features/contracts/hooks/index.ts +19 -0
- package/src/features/contracts/hooks/useAddContracts.ts +27 -0
- package/src/features/contracts/hooks/useContractListing.ts +71 -0
- package/src/features/contracts/hooks/useContractSelection.ts +53 -0
- package/src/features/contracts/hooks/useContractSwitchChannel.ts +49 -0
- package/src/features/contracts/hooks/useListAvailableContracts.ts +27 -0
- package/src/features/contracts/hooks/useRemoveContracts.ts +21 -0
- package/src/features/contracts/hooks/useSetDefaultContract.ts +21 -0
- package/src/features/contracts/hooks/useSwitchContract.ts +76 -0
- package/src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx +316 -0
- package/src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx +415 -0
- package/src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx +83 -0
- package/src/features/contracts/layouts/ContractListingLayout/contract-listing-layout.scss +468 -0
- package/src/features/contracts/layouts/ContractListingLayout/utils.ts +4 -0
- package/src/features/contracts/layouts/index.ts +8 -0
- package/src/features/contracts/services/__tests__/contracts-client.test.ts +148 -0
- package/src/features/contracts/services/__tests__/list-attached-contracts.service.test.ts +95 -0
- package/src/features/contracts/services/add-contracts.service.ts +27 -0
- package/src/features/contracts/services/index.ts +24 -0
- package/src/features/contracts/services/list-attached-contracts.service.ts +40 -0
- package/src/features/contracts/services/list-available-contracts.service.ts +32 -0
- package/src/features/contracts/services/remove-contract.service.ts +27 -0
- package/src/features/contracts/services/remove-contracts.service.ts +36 -0
- package/src/features/contracts/services/set-default-contract.service.ts +27 -0
- package/src/features/contracts/types/ContractData.ts +1 -0
- package/src/features/contracts/utils/__tests__/changeContractToken.test.ts +141 -0
- package/src/features/contracts/utils/changeContractToken.ts +122 -0
- package/src/features/contracts/utils/clearPersistedSessionState.ts +74 -0
- package/src/features/org-units/components/OrgUnitDetailsNavbar/OrgUnitDetailsNavbar.tsx +45 -3
- package/src/features/org-units/components/OrgUnitDetailsNavbar/org-unit-details-navbar.scss +35 -0
- package/src/features/org-units/layouts/OrgUnitsLayout/OrgUnitsLayout.tsx +54 -48
- package/src/features/roles/layout/RolesLayout/RolesLayout.tsx +32 -23
- package/src/features/shared/components/BuyerPortalProvider/BuyerPortalProvider.tsx +1 -0
- package/src/features/shared/components/HeaderInside/HeaderInside.tsx +8 -1
- package/src/features/shared/components/HeaderInside/header-inside.scss +9 -0
- package/src/features/shared/layouts/BaseTabsLayout/SidebarMenu.tsx +6 -97
- package/src/features/shared/layouts/ContractTabsLayout/ContractTabsLayout.tsx +42 -5
- package/src/features/shared/layouts/LoadingTabsLayout/LoadingTabsLayout.tsx +1 -1
- package/src/features/shared/layouts/SumaPageLayout/FullSidebarNav.tsx +124 -0
- package/src/features/shared/layouts/SumaPageLayout/SumaPageLayout.tsx +56 -0
- package/src/features/shared/layouts/SumaPageLayout/SumaSidebar.tsx +80 -0
- package/src/features/shared/layouts/SumaPageLayout/SumaSidebarDrawer.tsx +48 -0
- package/src/features/shared/layouts/SumaPageLayout/index.ts +6 -0
- package/src/features/shared/layouts/SumaPageLayout/suma-sidebar-drawer.scss +37 -0
- package/src/features/shared/layouts/index.ts +6 -0
- package/src/features/shared/services/feature-flags/__tests__/get-feature-flags.service.test.ts +71 -0
- package/src/features/shared/services/feature-flags/get-feature-flags.service.ts +1 -0
- package/src/features/shared/utils/buyerPortalRoutes.ts +9 -0
- package/src/features/shared/utils/constants.ts +1 -1
- package/src/features/shared/utils/getSumaListingSidebarLinks.ts +18 -0
- package/src/features/shared/utils/index.ts +1 -0
- package/src/features/users/layouts/UsersLayout/UsersLayout.tsx +97 -103
- package/src/pages/contract-information.tsx +128 -0
- package/src/pages/contracts.tsx +104 -0
- package/src/pages/home.tsx +10 -6
- package/src/pages/org-unit-details.tsx +17 -13
- package/src/themes/layouts.scss +2 -0
- package/.agents/skills/create-page/SKILL.md +0 -154
- package/.agents/skills/create-page/metadata.json +0 -14
- package/.specify/memory/constitution.md +0 -83
package/docs/superpowers/specs/2026-06-26-contracts-listing-selection-search-pagination-design.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Contracts listing — selection, search & local pagination
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-06-26
|
|
4
|
+
**Status:** Approved (design)
|
|
5
|
+
**Branch:** `feat/suma-switching`
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
The SUMA contracts listing (`ContractListingLayout`) shows all contracts for an org
|
|
10
|
+
unit. We just wired clicking a contract to a real commercial-context switch
|
|
11
|
+
(token/session change + navigation to the profile page).
|
|
12
|
+
|
|
13
|
+
The problem: the **entire row** has a single `onClick` that triggers the switch, and the
|
|
14
|
+
per-row checkbox is **display-only**. So clicking a checkbox switches contracts instead
|
|
15
|
+
of selecting — the multi-select (future bulk-delete) feature is unusable. The reference
|
|
16
|
+
screenshots also show a richer listing experience that isn't built yet: a search field,
|
|
17
|
+
local pagination ("1 — 25 of 50" with prev/next arrows), a "Select all (N)" row with an
|
|
18
|
+
indeterminate state, and a floating selection toolbar ("N selected" · ✕ · Remove).
|
|
19
|
+
|
|
20
|
+
This change builds that full listing UX while preserving the contract-switch behavior.
|
|
21
|
+
|
|
22
|
+
## Goals
|
|
23
|
+
|
|
24
|
+
- Separate **checkbox click → select** from **row-body click → switch contract**.
|
|
25
|
+
- Add **search** (local, case-insensitive over name/email).
|
|
26
|
+
- Add **local pagination**: 25 per page, `<` / `>` arrows, `"{from} — {to} of {total}"`.
|
|
27
|
+
- Add a **"Select all (N)"** row (tri-state: empty / indeterminate / checked) scoped to
|
|
28
|
+
**all filtered contracts** (across pages).
|
|
29
|
+
- Add a **floating selection bar** ("N selected" · ✕ clear · **Remove**). Remove is a
|
|
30
|
+
**no-op placeholder** (bulk delete is a future feature).
|
|
31
|
+
- Selection **persists** across search and page changes.
|
|
32
|
+
|
|
33
|
+
## Non-goals
|
|
34
|
+
|
|
35
|
+
- Implementing actual bulk contract removal (Remove stays a placeholder).
|
|
36
|
+
- Server-side search/pagination (all contracts are already loaded client-side).
|
|
37
|
+
- Changing the contract-switch flow itself (token/session) — already done.
|
|
38
|
+
|
|
39
|
+
## Decisions (confirmed)
|
|
40
|
+
|
|
41
|
+
| Question | Decision |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| Scope | Full listing UX from the screenshots |
|
|
44
|
+
| Remove button | No-op placeholder |
|
|
45
|
+
| Page size / control | 25 per page, prev/next arrows + interval counter |
|
|
46
|
+
| Select-all scope | All filtered contracts; selection persists across pages/search |
|
|
47
|
+
| Shift-range select | Keep, moved onto the checkbox |
|
|
48
|
+
|
|
49
|
+
## Interaction model
|
|
50
|
+
|
|
51
|
+
- **Checkbox click** → toggles that row's selection. Calls `stopPropagation()` so it
|
|
52
|
+
never triggers a switch. `event.shiftKey` → range select; otherwise plain toggle.
|
|
53
|
+
- **Row body click** (avatar + name) → switches contract (existing `useSwitchContract`
|
|
54
|
+
flow). The `loading` guard and error toast stay.
|
|
55
|
+
- The ⌘/Ctrl-click-on-row toggle is **removed** (selection now lives on the checkbox).
|
|
56
|
+
- The ⌘/Ctrl + **A** keyboard select-all shortcut is **kept** (operates on filtered IDs).
|
|
57
|
+
- Selection state is a `Set<string>` of contract IDs that survives filtering and paging.
|
|
58
|
+
|
|
59
|
+
## Architecture
|
|
60
|
+
|
|
61
|
+
All data is local — the layout already receives the full `contracts` array.
|
|
62
|
+
|
|
63
|
+
### New hook: `useContractListing`
|
|
64
|
+
`src/features/contracts/hooks/useContractListing.ts`
|
|
65
|
+
- Input: `{ contracts: ContractData[]; pageSize?: number /* default 25 */ }`.
|
|
66
|
+
- State: `searchTerm`, `page`.
|
|
67
|
+
- Derived (via pure helpers, see below): `filtered`, `total`, `pageItems`, `from`, `to`,
|
|
68
|
+
`hasPrev`, `hasNext`, `filteredIds`.
|
|
69
|
+
- Actions: `setSearch(term)` (resets `page` to 1), `nextPage()`, `prevPage()`.
|
|
70
|
+
- Clamps `page` to valid range when the filtered set shrinks.
|
|
71
|
+
|
|
72
|
+
### New pure helpers (testable in node env)
|
|
73
|
+
`src/features/contracts/hooks/contractListingHelpers.ts`
|
|
74
|
+
- `filterContracts(contracts, term)` — case-insensitive substring match on
|
|
75
|
+
`name ?? ""` and `email ?? ""`; empty/whitespace term returns all.
|
|
76
|
+
- `paginate(items, page, pageSize)` → `{ pageItems, from, to, total, hasPrev, hasNext }`
|
|
77
|
+
(`from`/`to` are 1-based for display; `from = to = 0` when empty).
|
|
78
|
+
|
|
79
|
+
### Reuse: `useContractSelection` (unchanged)
|
|
80
|
+
`handleSelectAll(filteredIds)`, `handleModifierClick(id)` (toggle),
|
|
81
|
+
`handleShiftClick(id, filteredIds)` (range), `clearSelection()`. Select-all and range are
|
|
82
|
+
fed **`filteredIds`** (not just the current page).
|
|
83
|
+
|
|
84
|
+
### Components
|
|
85
|
+
- **`ContractListItem`** (edit) — checkbox is an interactive `role="checkbox"`
|
|
86
|
+
(`aria-checked`) element with `onClick` → `onSelect(id, event)` + `stopPropagation()`.
|
|
87
|
+
Row body retains the switch handler. New/changed props: replace
|
|
88
|
+
`onModifierClick`/`onShiftClick` with a single `onSelect(id, event)`; layout maps
|
|
89
|
+
`event.shiftKey ? handleShiftClick : handleModifierClick`.
|
|
90
|
+
- **`ContractsListHeader`** (new) —
|
|
91
|
+
`src/features/contracts/components/ContractsListHeader/`. Left: `InternalSearch`
|
|
92
|
+
(`textSearch={setSearch}`, `defaultValue={searchTerm}`). Right: pagination control —
|
|
93
|
+
`"{from} — {to} of {total}"` and two icon-buttons (prev/next), disabled at the ends.
|
|
94
|
+
- **`ContractSelectAllRow`** (new) —
|
|
95
|
+
`src/features/contracts/components/ContractSelectAllRow/`. Tri-state checkbox
|
|
96
|
+
(`checked` = all filtered selected, `indeterminate` = some, else empty) + label
|
|
97
|
+
`"Select all ({total})"`. Toggle: if any selected → `clearSelection()`, else
|
|
98
|
+
`handleSelectAll(filteredIds)`.
|
|
99
|
+
- **`ContractSelectionBar`** (new) —
|
|
100
|
+
`src/features/contracts/components/ContractSelectionBar/`. Floating pill rendered when
|
|
101
|
+
`selectedIds.size > 0`: `✕` (→ `clearSelection`), `"{n} selected"`, `Remove` button
|
|
102
|
+
(red; **no-op** for now — `onClick={() => {}}` with a `// TODO: bulk delete` note).
|
|
103
|
+
- **`ContractSelectionList`** (edit) — renders `pageItems` (not all contracts), forwards
|
|
104
|
+
`onSelect`. The old inline `[data-fs-bp-contract-selection-toolbar]` is removed in favor
|
|
105
|
+
of `ContractSelectionBar`.
|
|
106
|
+
|
|
107
|
+
### Layout composition (`ContractListingLayout`, multi-contract branch)
|
|
108
|
+
```
|
|
109
|
+
<ContractsListHeader … /> // search + pagination
|
|
110
|
+
<ContractSelectAllRow … /> // tri-state select all
|
|
111
|
+
<ContractSelectionList items=pageItems onSelect … />
|
|
112
|
+
<ContractSelectionBar selectedCount … onClear onRemove /> // floating, when selection > 0
|
|
113
|
+
```
|
|
114
|
+
The single-contract branch (BasicCard) is unchanged.
|
|
115
|
+
|
|
116
|
+
## Styling
|
|
117
|
+
|
|
118
|
+
Extend `contract-listing-layout.scss`:
|
|
119
|
+
- `[data-fs-bp-contracts-list-header]` — flex row, search left / pagination right; arrow
|
|
120
|
+
icon-buttons with disabled state.
|
|
121
|
+
- `[data-fs-bp-contract-select-all-row]` — checkbox + label row above the list.
|
|
122
|
+
- Tri-state checkbox visuals (checked + indeterminate "minus"); reuse existing
|
|
123
|
+
`[data-fs-bp-contract-list-item-checkbox]` look, add an interactive/focus state.
|
|
124
|
+
- `[data-fs-bp-contract-selection-bar]` — fixed, bottom-center, rounded, shadowed pill
|
|
125
|
+
(repurpose the removed toolbar's count/remove styles).
|
|
126
|
+
|
|
127
|
+
## Icons
|
|
128
|
+
|
|
129
|
+
Pagination arrows and the indeterminate "minus"/clear "✕" use `@faststore/ui`'s `Icon`
|
|
130
|
+
(as `InternalSearch` does with `X` / `MagnifyingGlass`). Exact sprite symbol names
|
|
131
|
+
(arrow-left/right, minus) verified against `/buyer-portal-icons.svg` and `@faststore/ui`
|
|
132
|
+
during implementation; fall back to existing confirmed names if needed.
|
|
133
|
+
|
|
134
|
+
## Testing
|
|
135
|
+
|
|
136
|
+
- `contractListingHelpers.test.ts` (node env, pure functions):
|
|
137
|
+
- `filterContracts`: matches name and email case-insensitively; empty term → all;
|
|
138
|
+
no-match → empty.
|
|
139
|
+
- `paginate`: correct slice/from/to/hasPrev/hasNext for first/middle/last/empty pages;
|
|
140
|
+
page clamped past the end.
|
|
141
|
+
- Existing `useContractSelection` helper tests still pass (toggle/range/select-all).
|
|
142
|
+
- `make check` (lint + typecheck + test). Note: typecheck has ~133 pre-existing,
|
|
143
|
+
environment-related errors from the linked `@faststore/core`; the bar is **no new
|
|
144
|
+
errors in touched files**.
|
|
145
|
+
|
|
146
|
+
## Manual verification (linked to `b2bfaststoredev.store`)
|
|
147
|
+
|
|
148
|
+
1. Org unit with >25 contracts: list shows 25, counter `"1 — 25 of N"`, `>` enabled.
|
|
149
|
+
2. Click `>` / `<` → page changes, counter updates, `<` disabled on page 1, `>` on last.
|
|
150
|
+
3. Type in search → list filters live, resets to page 1, counter reflects filtered total.
|
|
151
|
+
4. Click a **checkbox** → row selects, **no switch**; floating bar shows `"1 selected"`.
|
|
152
|
+
5. Shift-click another checkbox → range selected.
|
|
153
|
+
6. "Select all (N)" → all filtered selected (indeterminate when partial); ✕ clears.
|
|
154
|
+
7. Selection persists when changing page / editing search.
|
|
155
|
+
8. Click a **row body** (name) → contract switches (cookie/JWT changes, navigates to
|
|
156
|
+
profile) — unchanged from the prior change.
|
|
157
|
+
9. Remove button is visible but does nothing.
|
package/package.json
CHANGED
package/plugin.config.js
CHANGED
|
@@ -88,6 +88,14 @@ module.exports = {
|
|
|
88
88
|
path: "/pvt/organization-account/accounting-field/[orgUnitId]/[contractId]/[accountingFieldId]",
|
|
89
89
|
appLayout: false,
|
|
90
90
|
},
|
|
91
|
+
contracts: {
|
|
92
|
+
path: "/pvt/organization-account/contracts/[orgUnitId]",
|
|
93
|
+
appLayout: false,
|
|
94
|
+
},
|
|
95
|
+
"contract-information": {
|
|
96
|
+
path: "/pvt/organization-account/contract-information/[orgUnitId]/[contractId]",
|
|
97
|
+
appLayout: false,
|
|
98
|
+
},
|
|
91
99
|
},
|
|
92
100
|
|
|
93
101
|
apis: {
|
|
@@ -283,6 +283,28 @@
|
|
|
283
283
|
d="M154-128q-43.72 0-74.86-31.14Q48-190.27 48-234v-492q0-43.72 31.14-74.86T154-832h220l106 106h326q43.72 0 74.86 31.14T912-620v386q0 43.73-31.14 74.86Q849.72-128 806-128H154Zm339.67-136 104.16-79.68L702-264l-38-130 104-84H638l-40-126-40 126H428l104.17 84.23L493.67-264Z" />
|
|
284
284
|
</symbol>
|
|
285
285
|
|
|
286
|
+
<symbol
|
|
287
|
+
id="Star"
|
|
288
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
289
|
+
viewBox="0 0 24 24"
|
|
290
|
+
fill="currentColor"
|
|
291
|
+
>
|
|
292
|
+
<path
|
|
293
|
+
d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"
|
|
294
|
+
fill="currentColor" />
|
|
295
|
+
</symbol>
|
|
296
|
+
|
|
297
|
+
<symbol
|
|
298
|
+
id="StarOutline"
|
|
299
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
300
|
+
viewBox="0 0 24 24"
|
|
301
|
+
fill="currentColor"
|
|
302
|
+
>
|
|
303
|
+
<path
|
|
304
|
+
d="M22 9.24l-7.19-.62L12 2 9.19 8.62 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"
|
|
305
|
+
fill="currentColor" />
|
|
306
|
+
</symbol>
|
|
307
|
+
|
|
286
308
|
<symbol id="LoadingIndicator" xmlns="http://www.w3.org/2000/svg"
|
|
287
309
|
viewBox="0 0 18 18" fill="none">
|
|
288
310
|
<path
|
|
@@ -606,4 +628,18 @@
|
|
|
606
628
|
<path d="M24.771 32.622C24.9723 32.4213 25.073 32.1643 25.073 31.851C25.073 31.5377 24.9727 31.2803 24.772 31.079C24.5713 30.8777 24.3143 30.777 24.001 30.777C23.6877 30.777 23.4303 30.8773 23.229 31.078C23.0277 31.2787 22.927 31.5357 22.927 31.849C22.927 32.1623 23.0273 32.4197 23.228 32.621C23.4287 32.8223 23.6857 32.923 23.999 32.923C24.3123 32.923 24.5697 32.8227 24.771 32.622ZM23.304 26.4655H24.8425V14.1615H23.304V26.4655ZM24.02 42C21.514 42 19.1758 41.5277 17.0055 40.583C14.8348 39.6383 12.9307 38.348 11.293 36.712C9.65533 35.076 8.36383 33.1725 7.4185 31.0015C6.47283 28.8302 6 26.4902 6 23.9815C6 21.4985 6.47233 19.164 7.417 16.978C8.36167 14.7923 9.652 12.889 11.288 11.268C12.924 9.647 14.8275 8.36383 16.9985 7.4185C19.1698 6.47283 21.5098 6 24.0185 6C26.5015 6 28.836 6.47233 31.022 7.417C33.2077 8.36167 35.111 9.64367 36.732 11.263C38.353 12.8823 39.6362 14.7867 40.5815 16.976C41.5272 19.165 42 21.4997 42 23.98C42 26.486 41.5277 28.8242 40.583 30.9945C39.6383 33.1652 38.3563 35.0663 36.737 36.698C35.1177 38.3293 33.2133 39.6208 31.024 40.5725C28.835 41.5242 26.5003 42 24.02 42ZM24.025 40.4615C28.5877 40.4615 32.4678 38.8608 35.6655 35.6595C38.8628 32.4582 40.4615 28.5633 40.4615 23.975C40.4615 19.4123 38.866 15.5322 35.675 12.3345C32.4837 9.13717 28.592 7.5385 24 7.5385C19.4283 7.5385 15.5418 9.134 12.3405 12.325C9.13917 15.5163 7.5385 19.408 7.5385 24C7.5385 28.5717 9.13917 32.4582 12.3405 35.6595C15.5418 38.8608 19.4367 40.4615 24.025 40.4615Z" fill="currentColor"/>
|
|
607
629
|
</g>
|
|
608
630
|
</symbol>
|
|
609
|
-
|
|
631
|
+
<symbol
|
|
632
|
+
id="Sparkle"
|
|
633
|
+
viewBox="0 -960 960 960"
|
|
634
|
+
fill="currentColor"
|
|
635
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
636
|
+
>
|
|
637
|
+
<path fill="currentColor"
|
|
638
|
+
d="M480-80q0-83-31.5-156T363-363q-54-54-127-85.5T80-480q83 0 156-31.5T363-597q54-54 85.5-127T480-880q0 83 31.5 156T597-597q54 54 127 85.5T880-480q-83 0-156 31.5T597-363q-54 54-85.5 127T480-80Z" />
|
|
639
|
+
</symbol>
|
|
640
|
+
<symbol id="InfoSolid" xmlns="http://www.w3.org/2000/svg"
|
|
641
|
+
viewBox="0 0 256 256"
|
|
642
|
+
fill="currentColor">
|
|
643
|
+
<path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm-4,48a12,12,0,1,1-12,12A12,12,0,0,1,124,72Zm12,112a16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40a8,8,0,0,1,0,16Z" fill="currentColor"></path>
|
|
644
|
+
</symbol>
|
|
645
|
+
</svg>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Specification Quality Checklist: SUMA Phase 2 — Contract Listing, Switching & Management
|
|
2
|
+
|
|
3
|
+
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
|
4
|
+
**Created**: 2026-06-15
|
|
5
|
+
**Feature**: [spec.md](../spec.md)
|
|
6
|
+
|
|
7
|
+
## Content Quality
|
|
8
|
+
|
|
9
|
+
- [x] No implementation details (languages, frameworks, APIs)
|
|
10
|
+
- [x] Focused on user value and business needs
|
|
11
|
+
- [x] Written for non-technical stakeholders
|
|
12
|
+
- [x] All mandatory sections completed
|
|
13
|
+
|
|
14
|
+
## Requirement Completeness
|
|
15
|
+
|
|
16
|
+
- [x] No [NEEDS CLARIFICATION] markers remain
|
|
17
|
+
- [x] Requirements are testable and unambiguous
|
|
18
|
+
- [x] Success criteria are measurable
|
|
19
|
+
- [x] Success criteria are technology-agnostic (no implementation details)
|
|
20
|
+
- [x] All acceptance scenarios are defined
|
|
21
|
+
- [x] Edge cases are identified
|
|
22
|
+
- [x] Scope is clearly bounded
|
|
23
|
+
- [x] Dependencies and assumptions identified
|
|
24
|
+
|
|
25
|
+
## Feature Readiness
|
|
26
|
+
|
|
27
|
+
- [x] All functional requirements have clear acceptance criteria
|
|
28
|
+
- [x] User scenarios cover primary flows
|
|
29
|
+
- [x] Feature meets measurable outcomes defined in Success Criteria
|
|
30
|
+
- [x] No implementation details leak into specification
|
|
31
|
+
|
|
32
|
+
## Notes
|
|
33
|
+
|
|
34
|
+
- All checklist items pass. Spec is ready for `/speckit-plan`.
|
|
35
|
+
- Session integration with Identity/Auth team is flagged as an assumption — confirm before coding begins.
|
|
36
|
+
- UX prototype approval is assumed; verify no late-breaking changes before implementation.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# BFF API Contract: Contracts Domain
|
|
2
|
+
|
|
3
|
+
**Feature**: SUMA Phase 2
|
|
4
|
+
**Date**: 2026-06-15
|
|
5
|
+
**Updated**: 2026-06-18 (confirmed BFF v1.0.3; activate endpoint removed — not a BFF concept)
|
|
6
|
+
**Base path**: `/_v/store-front/` (via `getApiUrl()` — see `src/features/shared/utils/constants.ts`)
|
|
7
|
+
|
|
8
|
+
All storefront endpoints are relative to this base. The client (`ContractsClient.ts`) uses paths like `units/{orgUnitId}/contracts/attached` which expand to `/_v/store-front/units/{orgUnitId}/contracts/attached`.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Existing Endpoints (no changes needed)
|
|
13
|
+
|
|
14
|
+
### Get Contract Details
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
GET units/{orgUnitId}/contracts/{contractId}
|
|
18
|
+
Auth: Cookie header
|
|
19
|
+
|
|
20
|
+
Response 200:
|
|
21
|
+
{
|
|
22
|
+
contract: ContractData
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Toggle Contract Active Status
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
POST units/{unitId}/contracts/{contractId}/status
|
|
30
|
+
Auth: Cookie header
|
|
31
|
+
Body: { isActive: boolean }
|
|
32
|
+
|
|
33
|
+
Response 200:
|
|
34
|
+
{ isActive: boolean }
|
|
35
|
+
|
|
36
|
+
Note: Toggles whether a contract is enabled in the unit.
|
|
37
|
+
Distinct from contract switching (session scope).
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Storefront Endpoints — BFF v1.0.2 (Confirmed)
|
|
43
|
+
|
|
44
|
+
These replace the previously speculative SUMA Phase 2 endpoints.
|
|
45
|
+
|
|
46
|
+
### List Attached Contracts
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
GET units/{orgUnitId}/contracts/attached
|
|
50
|
+
GET units/{orgUnitId}/contracts/attached?details=true ← include full contract objects
|
|
51
|
+
Auth: Cookie header
|
|
52
|
+
|
|
53
|
+
Response 200 (without ?details):
|
|
54
|
+
{
|
|
55
|
+
contractIds: string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Response 200 (with ?details=true):
|
|
59
|
+
{
|
|
60
|
+
contracts: ContractData[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Note: Returns contracts within this unit's contractIds scope.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### List Available Contracts
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
GET units/{orgUnitId}/contracts/available
|
|
70
|
+
Auth: Cookie header
|
|
71
|
+
|
|
72
|
+
Response 200:
|
|
73
|
+
{
|
|
74
|
+
contractIds: string[]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Note: Returns the allowed contract ID list from the org root unit's
|
|
78
|
+
contractIds scope — i.e., the superset a child unit can attach from.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Add Contracts to Unit
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
POST units/{orgUnitId}/contracts/attached
|
|
85
|
+
Auth: Cookie header
|
|
86
|
+
Body: {
|
|
87
|
+
ids: string[]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Constraints:
|
|
91
|
+
- IDs must be within the root unit's contractIds scope (available contracts)
|
|
92
|
+
- Cap: max 20 IDs per unit total after merge
|
|
93
|
+
|
|
94
|
+
Response 200:
|
|
95
|
+
{ ... } ← shape TBD; confirm with BFF team
|
|
96
|
+
|
|
97
|
+
Frontend behavior:
|
|
98
|
+
- Success → refresh attached contracts list
|
|
99
|
+
- Error → show error toast
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Remove One Contract from Unit
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
DELETE units/{orgUnitId}/contracts/attached/{contractId}
|
|
106
|
+
Auth: Cookie header
|
|
107
|
+
|
|
108
|
+
Note: Per-resource deletion (one contract at a time).
|
|
109
|
+
For bulk removal, call in sequence for each contractId.
|
|
110
|
+
|
|
111
|
+
Response 200 / 204:
|
|
112
|
+
(empty or minimal confirmation)
|
|
113
|
+
|
|
114
|
+
Frontend guards (enforced before calling):
|
|
115
|
+
- Cannot remove if resulting count would be 0 (UI blocks)
|
|
116
|
+
- Remove option hidden when unit has exactly 1 contract
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Management Endpoints — BFF v1.0.2 (Admin-Only)
|
|
122
|
+
|
|
123
|
+
These use a **different base path** (`/_v/contracts-management/`) and are not reachable
|
|
124
|
+
via the standard `ContractsClient` which targets `/_v/store-front/`.
|
|
125
|
+
|
|
126
|
+
A separate client (e.g., `ContractsManagementClient`) with a custom URL is required.
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
GET /_v/contracts-management/unit-contracts/{unitId}
|
|
130
|
+
POST /_v/contracts-management/unit-contracts/{unitId} Body: { ids: string[] }
|
|
131
|
+
DELETE /_v/contracts-management/unit-contracts/{unitId}/{contractId}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
These are admin-only maintenance operations for the root unit `contractIds` scope.
|
|
135
|
+
The storefront endpoints (above) are used by buyers and respect the scope set here.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## TBD Endpoints (Not yet confirmed)
|
|
140
|
+
|
|
141
|
+
| Operation | Original speculative path | Status |
|
|
142
|
+
| ---------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
143
|
+
| Activate contract (switch session) | — | **Not a BFF concept.** In SUMA, "navigating to a contract" is purely client-side routing (`router.push`). No BFF call needed. Confirmed after checking BFF v1.0.3 routes and clarifying the SUMA mental model: same user, multiple contracts, all simultaneously accessible. |
|
|
144
|
+
| Set default contract | `POST units/{orgUnitId}/contracts/{contractId}/default` | TBD — pending BFF team confirmation. Frontend renders the "Set as default" menu item as disabled until this is confirmed. |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Frontend Client Methods to Add
|
|
149
|
+
|
|
150
|
+
These map to new methods in `src/features/contracts/clients/ContractsClient.ts`:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// List contracts in the unit's attached scope (with optional inline details)
|
|
154
|
+
listAttachedContracts(orgUnitId: string, cookie: string, details?: boolean)
|
|
155
|
+
→ Promise<{ contractIds: string[] } | { contracts: ContractData[] }>
|
|
156
|
+
|
|
157
|
+
// List contracts available to attach (from root unit scope)
|
|
158
|
+
listAvailableContracts(orgUnitId: string, cookie: string)
|
|
159
|
+
→ Promise<{ contractIds: string[] }>
|
|
160
|
+
|
|
161
|
+
// Add contracts to unit scope (max 20 total; must be within root scope)
|
|
162
|
+
addContracts(orgUnitId: string, ids: string[], cookie: string)
|
|
163
|
+
→ Promise<unknown> // shape TBD
|
|
164
|
+
|
|
165
|
+
// Remove one contract from unit scope (call per contractId for bulk)
|
|
166
|
+
removeContract(orgUnitId: string, contractId: string, cookie: string)
|
|
167
|
+
→ Promise<void>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Page Routes (Frontend)
|
|
173
|
+
|
|
174
|
+
| Page | Route | File |
|
|
175
|
+
| -------------------- | ------------------------------------------------------------------------- | ------------------------------------ |
|
|
176
|
+
| Contract Listing | `/pvt/organization-account/contracts/[orgUnitId]` | `src/pages/contracts.tsx` |
|
|
177
|
+
| Contract Information | `/pvt/organization-account/contract-information/[orgUnitId]/[contractId]` | `src/pages/contract-information.tsx` |
|
|
178
|
+
|
|
179
|
+
Both registered in `plugin.config.js` and exposed via `buyerPortalRoutes.ts`:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
contracts: (params: { orgUnitId: string }) =>
|
|
183
|
+
replaceParams(`${base}/contracts/[orgUnitId]`, params),
|
|
184
|
+
|
|
185
|
+
contractInformation: (params: { orgUnitId: string; contractId: string }) =>
|
|
186
|
+
replaceParams(`${base}/contract-information/[orgUnitId]/[contractId]`, params),
|
|
187
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Feature Flag Contract: suma
|
|
2
|
+
|
|
3
|
+
**Feature**: SUMA Phase 2
|
|
4
|
+
**Date**: 2026-06-15
|
|
5
|
+
**Updated**: 2026-06-17
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Flag Definition
|
|
10
|
+
|
|
11
|
+
| Property | Value |
|
|
12
|
+
|----------|-------|
|
|
13
|
+
| FeatureHub key | `suma` |
|
|
14
|
+
| BFF constant array | `BUYER_PORTAL_FEATURE_FLAGS` in `flags.constants.ts` |
|
|
15
|
+
| Frontend TypeScript property | `suma?: boolean` |
|
|
16
|
+
| Type | Boolean (Standard Flag) |
|
|
17
|
+
| Default (flag off) | `false` — original navigation preserved |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Behavior
|
|
22
|
+
|
|
23
|
+
| Flag state | Homepage redirect target | Navigation |
|
|
24
|
+
|------------|-------------------------|-----------|
|
|
25
|
+
| `false` (off) | `org-unit-details` (existing behavior) | Unchanged |
|
|
26
|
+
| `true` (on) | `contracts` (Contract Listing — new) | SUMA Phase 2 flows active |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Where to Update
|
|
31
|
+
|
|
32
|
+
### 1. BFF (not in this repo)
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// buyer-portal-graphql/node/modules/flags/flags.constants.ts
|
|
36
|
+
export const BUYER_PORTAL_FEATURE_FLAGS = [
|
|
37
|
+
'enable_alternative_login',
|
|
38
|
+
'suma', // ADD
|
|
39
|
+
] as const;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Frontend type definition
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// src/features/shared/components/BuyerPortalProvider/BuyerPortalProvider.tsx
|
|
46
|
+
export type FeatureFlags = {
|
|
47
|
+
enableAlternativeLogin?: boolean;
|
|
48
|
+
authSettings?: AuthSettingsFeatureFlags;
|
|
49
|
+
suma?: boolean; // ADD
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3. Flag normalization in get-feature-flags.service.ts
|
|
54
|
+
|
|
55
|
+
The flag key `suma` matches the TypeScript property name directly — no snake_case → camelCase conversion needed. Add it to `getBuyerPortalFeatureFlags()`:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// src/features/shared/services/feature-flags/get-feature-flags.service.ts
|
|
59
|
+
export async function getBuyerPortalFeatureFlags(cookie: string): Promise<FeatureFlags> {
|
|
60
|
+
const raw = await fetchBuyerPortalFlags(cookie);
|
|
61
|
+
const normalized = normalizeFlagsEndpointBooleans(raw);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
enableAlternativeLogin: normalized["enable_alternative_login"] ?? false,
|
|
65
|
+
authSettings: parseAuthSettingsFromFlags(raw),
|
|
66
|
+
suma: normalized["suma"] ?? false, // ADD
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 4. Usage in home.tsx
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// src/pages/home.tsx — conditional redirect
|
|
75
|
+
if (featureFlags?.suma) {
|
|
76
|
+
// redirect to contracts listing
|
|
77
|
+
Location: buyerPortalRoutes.contracts({ orgUnitId: userOrgUnit.id })
|
|
78
|
+
} else {
|
|
79
|
+
// existing redirect
|
|
80
|
+
Location: buyerPortalRoutes.orgUnitDetails({ orgUnitId: userOrgUnit.id })
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Rollout Plan
|
|
87
|
+
|
|
88
|
+
1. Create flag in FeatureHub dashboard under the Organization Account project with key `suma`
|
|
89
|
+
2. Enable in `dev` environment for testing
|
|
90
|
+
3. Enable in `prod` behind a % rollout after E2E validation
|
|
91
|
+
4. Full enable after stakeholder sign-off
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Data Model: SUMA Phase 2
|
|
2
|
+
|
|
3
|
+
**Feature**: Contract Listing, Switching & Management
|
|
4
|
+
**Date**: 2026-06-15
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Entities
|
|
9
|
+
|
|
10
|
+
### ContractData (extended)
|
|
11
|
+
|
|
12
|
+
The existing type extended with SUMA Phase 2 fields.
|
|
13
|
+
|
|
14
|
+
**File**: `src/features/contracts/types/ContractData.ts`
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
ContractData {
|
|
18
|
+
id: string -- unique identifier (from BFF)
|
|
19
|
+
name: string | null -- display name; fall back to email if null
|
|
20
|
+
email: string -- contact email for the contract
|
|
21
|
+
isActive: boolean -- whether the contract is enabled in this org unit
|
|
22
|
+
isDefault?: boolean -- NEW: whether this is the unit's default contract
|
|
23
|
+
creationDate: string -- ISO date string
|
|
24
|
+
salesRepresentative?: string -- optional
|
|
25
|
+
imageUrl?: string -- optional logo/image
|
|
26
|
+
assortment?: unknown[] -- optional, opaque
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Validation rules**:
|
|
31
|
+
- `id` must be non-empty
|
|
32
|
+
- `isDefault` defaults to `false` when absent (treat as optional for backward compat)
|
|
33
|
+
- At most one contract per org unit may have `isDefault: true`
|
|
34
|
+
|
|
35
|
+
**State transitions** (from Contract Listing perspective):
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Contract states:
|
|
39
|
+
active (session) ← activate() → inactive
|
|
40
|
+
default ← setDefault() → non-default
|
|
41
|
+
|
|
42
|
+
Multi-select states:
|
|
43
|
+
unselected → selected (via click modifiers) → unselected
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### ActiveContract (session concept)
|
|
49
|
+
|
|
50
|
+
Not a stored entity — derived from session. Represents which contract the current user's session is scoped to.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
ActiveContract {
|
|
54
|
+
contractId: string -- ID of the currently active contract
|
|
55
|
+
orgUnitId: string -- ID of the org unit
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Lifecycle**:
|
|
60
|
+
1. Loaded from session on page load (provided via `BuyerPortalContext`)
|
|
61
|
+
2. Updated via BFF `activate` call
|
|
62
|
+
3. Broadcast to other tabs via `BroadcastChannel` after successful activation
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### ContractSelection (UI state — not persisted)
|
|
67
|
+
|
|
68
|
+
Managed by `useContractSelection` hook. Not stored in BFF or localStorage.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
ContractSelection {
|
|
72
|
+
selectedIds: Set<string> -- set of selected contract IDs
|
|
73
|
+
lastSelectedIndex: number -- index of last Shift+Click anchor (for range selection)
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Transitions**:
|
|
78
|
+
- `handleSelectAll(ids)`: replace `selectedIds` with all provided IDs
|
|
79
|
+
- `handleModifierClick(id)`: toggle `id` in `selectedIds`
|
|
80
|
+
- `handleShiftClick(id, allIds)`: fill range from `lastSelectedIndex` to target index
|
|
81
|
+
- `clearSelection()`: empty `selectedIds`
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### OrgUnitBasicData (existing — relevant subset)
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
OrgUnitBasicData {
|
|
89
|
+
id: string
|
|
90
|
+
name: string
|
|
91
|
+
parentId?: string -- enables hierarchy navigation
|
|
92
|
+
children?: OrgUnitBasicData[]
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
No changes needed. Used by `OrgUnitBreadcrumb` for hierarchy navigation.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Relationships
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
OrgUnit 1 ──────────── * ContractData
|
|
104
|
+
│
|
|
105
|
+
├── 0..1 isDefault: true
|
|
106
|
+
└── 0..1 isActive (session)
|
|
107
|
+
|
|
108
|
+
ContractData * ──────── * OrgUnit (via propagate-to-children)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A single org unit holds many contracts. Exactly one may be the `isDefault`. The session's active contract is tracked separately as `ActiveContract`.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Feature Flag
|
|
116
|
+
|
|
117
|
+
**Key** (BFF / FeatureHub): `enable_suma_phase_2`
|
|
118
|
+
**TypeScript property**: `enableSumaPhase2?: boolean` (added to `FeatureFlags` in `BuyerPortalProvider.tsx`)
|
|
119
|
+
**Effect when true**: home page redirects to Contract Listing instead of org-unit-details
|