@vtex/faststore-plugin-buyer-portal 2.0.7 → 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 +30 -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/components/BasicBuyingPolicyDrawer/BasicBuyingPolicyDrawer.tsx +2 -2
- 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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Feature Specification: SUMA Phase 2 — Contract Listing Page
|
|
2
|
+
|
|
3
|
+
**Feature Branch**: `feat/suma-listing`
|
|
4
|
+
|
|
5
|
+
**Created**: 2026-06-17
|
|
6
|
+
|
|
7
|
+
**Status**: Done
|
|
8
|
+
|
|
9
|
+
**Parent spec**: `specs/001-suma-phase-2/spec.md` (User Stories 1 & 2)
|
|
10
|
+
|
|
11
|
+
**Depends on**: `feat/suma-infra` — feature flag, route registration, `listAttachedContractsService`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Business Context
|
|
16
|
+
|
|
17
|
+
### Problem
|
|
18
|
+
|
|
19
|
+
When the `suma` feature flag is enabled, the account homepage (`org-unit-details`) must be replaced by a dedicated Contract Listing page. Currently there is no such page. Without it, users with multiple contracts have no way to view, distinguish, or select their contracts under the SUMA Phase 2 flow.
|
|
20
|
+
|
|
21
|
+
The listing page is the entry point of the new flow: it shows all contracts available to the user's Organizational Unit, highlights the default contract, and supports multi-contract selection via keyboard shortcuts to enable future bulk admin actions (add/remove — implemented in a later task).
|
|
22
|
+
|
|
23
|
+
### Goals
|
|
24
|
+
|
|
25
|
+
- Replace `org-unit-details` as the account homepage when `featureFlags.suma === true`.
|
|
26
|
+
- Display all contracts for the Organizational Unit with clear visual hierarchy.
|
|
27
|
+
- Highlight the default contract (`isDefault === true`) with a visual badge.
|
|
28
|
+
- Support multi-select with keyboard shortcuts (Cmd/Ctrl+A, Cmd/Ctrl+Click, Shift+Click).
|
|
29
|
+
- Handle the single-contract edge case: hide removal actions when only one contract exists.
|
|
30
|
+
|
|
31
|
+
### Out of Scope
|
|
32
|
+
|
|
33
|
+
- Contract switching (Profile page as Contract Information) — separate task (`specs/suma-contract-switching.md`).
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## User Scenarios & Testing
|
|
38
|
+
|
|
39
|
+
### User Story 1 — View Contract Listing as Homepage (Priority: P1)
|
|
40
|
+
|
|
41
|
+
When the `suma` flag is on, a buyer navigating to the account homepage lands on the Contract Listing page instead of `org-unit-details`.
|
|
42
|
+
|
|
43
|
+
**Acceptance Scenarios**:
|
|
44
|
+
|
|
45
|
+
1. **Given** the `suma` feature flag is `true` and the user opens the account homepage, **When** the page loads, **Then** the Contract Listing page is displayed showing all contracts for the user's Organizational Unit.
|
|
46
|
+
2. **Given** the `suma` feature flag is `false`, **When** the user opens the account homepage, **Then** the original `org-unit-details` page is shown with no regression.
|
|
47
|
+
3. **Given** the loader is fetching contracts, **When** the data is pending, **Then** a loading skeleton is displayed.
|
|
48
|
+
4. **Given** the loader fails, **When** the page renders, **Then** the existing `ErrorTabsLayout` is shown.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### User Story 2 — Default Contract Highlighted (Priority: P1)
|
|
53
|
+
|
|
54
|
+
The contract with `isDefault === true` is visually distinguished from other contracts.
|
|
55
|
+
|
|
56
|
+
**Acceptance Scenarios**:
|
|
57
|
+
|
|
58
|
+
1. **Given** a contract has `isDefault: true`, **When** the listing renders, **Then** a "Default" badge is displayed on that contract's row.
|
|
59
|
+
2. **Given** no contract has `isDefault: true`, **When** the listing renders, **Then** no badge is shown and all rows look equal.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### User Story 3 — Multi-Select with Keyboard Shortcuts (Priority: P2)
|
|
64
|
+
|
|
65
|
+
A buyer on the Contract Listing page can select multiple contracts using keyboard shortcuts.
|
|
66
|
+
|
|
67
|
+
**Acceptance Scenarios**:
|
|
68
|
+
|
|
69
|
+
1. **Given** a buyer is on the Contract Listing page, **When** they press Cmd+A (Mac) or Ctrl+A (Windows/Linux), **Then** all contracts are selected without navigating anywhere.
|
|
70
|
+
2. **Given** a buyer is on the Contract Listing page, **When** they Cmd+Click / Ctrl+Click a contract, **Then** that contract is toggled in the selection without navigating to its detail.
|
|
71
|
+
3. **Given** a buyer has selected one contract and Shift+Clicks another, **Then** all contracts between the two are added to the selection as a range.
|
|
72
|
+
4. **Given** one or more contracts are selected, **When** the buyer clicks a contract without a modifier key, **Then** the selection is cleared and the click proceeds as normal.
|
|
73
|
+
5. **Given** contracts are selected, **When** the selection count is > 0, **Then** a bulk action toolbar appears showing the count and available actions.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### User Story 4 — Single-Contract State (Priority: P1)
|
|
78
|
+
|
|
79
|
+
When the Organizational Unit has exactly one contract, removal actions are hidden.
|
|
80
|
+
|
|
81
|
+
**Acceptance Scenarios**:
|
|
82
|
+
|
|
83
|
+
1. **Given** an Organizational Unit has exactly 1 contract, **When** the listing renders, **Then** no "Remove" button or bulk remove action is displayed.
|
|
84
|
+
2. **Given** an Organizational Unit has 2+ contracts, **When** contracts are selected, **Then** the bulk toolbar includes a "Remove" action.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### Key Scenarios
|
|
89
|
+
|
|
90
|
+
| Scenario | Pre-condition | Steps | Expected result |
|
|
91
|
+
|----------|--------------|-------|----------------|
|
|
92
|
+
| **Happy path** | Flag on, unit has 3 contracts, 1 is default | Load page | All 3 contracts listed; default has badge; no selection |
|
|
93
|
+
| **Single contract** | Flag on, unit has 1 contract | Load page | 1 contract shown; no remove action visible |
|
|
94
|
+
| **Cmd+A select all** | Flag on, 3 contracts | Press Cmd+A | All 3 highlighted; bulk toolbar shows count=3 |
|
|
95
|
+
| **Shift+Click range** | Flag on, 3 contracts | Click row 1, Shift+Click row 3 | Rows 1–3 highlighted |
|
|
96
|
+
| **Flag off** | Flag `suma=false` | Load account homepage | `org-unit-details` renders normally |
|
|
97
|
+
| **Load error** | Service throws | Page render | `ErrorTabsLayout` shown |
|
|
98
|
+
| **Empty list** | Unit has 0 contracts | Load page | Empty state shown |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Requirements
|
|
103
|
+
|
|
104
|
+
### Functional
|
|
105
|
+
|
|
106
|
+
- **FR-001**: Display all contracts from `listAttachedContractsService` for the current `orgUnitId`.
|
|
107
|
+
- **FR-002**: Show a "Default" badge on the contract where `isDefault === true`.
|
|
108
|
+
- **FR-003**: Show a loading skeleton while the loader is pending.
|
|
109
|
+
- **FR-004**: Show `ErrorTabsLayout` when the loader fails.
|
|
110
|
+
- **FR-005**: Show an empty state when the contract list is empty.
|
|
111
|
+
- **FR-006**: Support Cmd+A / Ctrl+A to select all contracts (no navigation).
|
|
112
|
+
- **FR-007**: Support Cmd+Click / Ctrl+Click to toggle individual contract selection (no navigation).
|
|
113
|
+
- **FR-008**: Support Shift+Click to select a contiguous range.
|
|
114
|
+
- **FR-009**: Show a bulk action toolbar (contract count) when `selectedIds.size > 0`.
|
|
115
|
+
- **FR-010**: Include a "Remove" action in the bulk toolbar only when `contracts.length > 1`.
|
|
116
|
+
- **FR-011**: Hide the "Remove" action entirely when `contracts.length === 1`.
|
|
117
|
+
- **FR-012**: Preserve existing `org-unit-details` behavior when `featureFlags.suma` is `false`.
|
|
118
|
+
|
|
119
|
+
### Non-functional
|
|
120
|
+
|
|
121
|
+
- Page must pass `make lint` and `make typecheck` (zero new errors).
|
|
122
|
+
- All new hooks and pure functions must have unit tests.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Arch Decisions
|
|
127
|
+
|
|
128
|
+
### Decision 1 — Data source
|
|
129
|
+
|
|
130
|
+
**Use `listAttachedContractsService`** (from `feat/suma-infra`, calls `GET units/{orgUnitId}/contracts/attached?details=true`) rather than the existing `getContractsByOrgUnitIdService`.
|
|
131
|
+
|
|
132
|
+
**Rationale**: The confirmed BFF v1.0.2 endpoint for SUMA is `/contracts/attached`. The old endpoint (`/contracts`) is used by the legacy flow and should not be conflated with the new one.
|
|
133
|
+
|
|
134
|
+
**Fallback**: If `listAttachedContractsService` returns null (error), render `ErrorTabsLayout`.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### Decision 2 — Selection state in a hook
|
|
139
|
+
|
|
140
|
+
**`useContractSelection`** manages `selectedIds: Set<string>` as local React state. It exposes `isSelected(id)`, `handleSelectAll(allIds)`, `handleModifierClick(id)`, `handleShiftClick(id, allIds)`, `clearSelection()`.
|
|
141
|
+
|
|
142
|
+
**Rationale**: Self-contained hook keeps the layout lean and allows unit testing without rendering. No global/shared state needed — selection is local to the listing page session.
|
|
143
|
+
|
|
144
|
+
**Cross-platform**: Uses `event.metaKey || event.ctrlKey` to detect Cmd/Ctrl on Mac and Windows/Linux.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### Decision 3 — Page structure mirrors `org-unit-details`
|
|
149
|
+
|
|
150
|
+
The new `contracts.tsx` page follows the exact same loader pattern as `org-unit-details.tsx`:
|
|
151
|
+
- `withLoaderErrorBoundary` wrapping a `withAuthLoader` callback
|
|
152
|
+
- Fetches `orgUnit`, `contracts`, and `user` in parallel
|
|
153
|
+
- Renders a layout component; passes error to `ErrorTabsLayout`
|
|
154
|
+
|
|
155
|
+
**Rationale**: Consistency with existing page structure. Reduces cognitive overhead for the team. Reuses `OrgUnitDetailsNavbar` and `OrgUnitBreadcrumb` for navigation continuity.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### Decision 4 — Single-contract gate at layout level
|
|
160
|
+
|
|
161
|
+
The `isSingleContract` flag (`contracts.length <= 1`) is computed in `ContractListingLayout` and passed as a prop to `ContractSelectionList` and (later) to remove-action components. This mirrors how `OrgUnitsDetailsLayout` handles the same condition today.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Technical Contract
|
|
166
|
+
|
|
167
|
+
### New Files
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
src/
|
|
171
|
+
├── pages/
|
|
172
|
+
│ └── contracts.tsx # NEW page
|
|
173
|
+
│
|
|
174
|
+
└── features/contracts/
|
|
175
|
+
├── hooks/
|
|
176
|
+
│ └── useContractSelection.ts # NEW hook
|
|
177
|
+
│
|
|
178
|
+
├── components/
|
|
179
|
+
│ ├── ContractListItem/
|
|
180
|
+
│ │ └── ContractListItem.tsx # NEW component
|
|
181
|
+
│ └── ContractSelectionList/
|
|
182
|
+
│ └── ContractSelectionList.tsx # NEW component
|
|
183
|
+
│
|
|
184
|
+
└── layouts/
|
|
185
|
+
└── ContractListingLayout/
|
|
186
|
+
└── ContractListingLayout.tsx # NEW layout
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
### `contracts.tsx` — Page
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// Loader data shape
|
|
195
|
+
type ContractsPageData = {
|
|
196
|
+
data: {
|
|
197
|
+
orgUnit: OrgUnitBasicData;
|
|
198
|
+
contracts: ContractData[]; // from listAttachedContractsService
|
|
199
|
+
user: UserData | null;
|
|
200
|
+
};
|
|
201
|
+
context: { clientContext: ClientContext };
|
|
202
|
+
hasError?: boolean;
|
|
203
|
+
error?: ErrorBoundaryProps;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Loader: parallel fetch of orgUnit, contracts, user
|
|
207
|
+
// Renders ContractListingLayout or ErrorTabsLayout
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Route: `/pvt/organization-account/contracts/[orgUnitId]` (registered in `plugin.config.js` via infra task).
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### `useContractSelection` — Hook
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
type UseContractSelectionReturn = {
|
|
218
|
+
selectedIds: Set<string>;
|
|
219
|
+
isSelected: (id: string) => boolean;
|
|
220
|
+
handleSelectAll: (allIds: string[]) => void;
|
|
221
|
+
handleModifierClick: (id: string) => void; // Cmd/Ctrl+Click
|
|
222
|
+
handleShiftClick: (id: string, allIds: string[]) => void;
|
|
223
|
+
clearSelection: () => void;
|
|
224
|
+
};
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Internal state: `selectedIds: Set<string>`, `lastSelectedIndex: number`.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
### `ContractListItem` — Component
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
type ContractListItemProps = {
|
|
235
|
+
contract: ContractData; // includes isDefault?: boolean
|
|
236
|
+
isSelected: boolean;
|
|
237
|
+
onModifierClick: (id: string, event: React.MouseEvent) => void;
|
|
238
|
+
onShiftClick: (id: string, event: React.MouseEvent) => void;
|
|
239
|
+
};
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Renders: contract name (fallback to email), "Default" badge when `isDefault === true`, selection highlight when `isSelected === true`.
|
|
243
|
+
|
|
244
|
+
Click behaviour:
|
|
245
|
+
- Plain click → no-op (activation TBD)
|
|
246
|
+
- Cmd/Ctrl+Click → calls `onModifierClick`
|
|
247
|
+
- Shift+Click → calls `onShiftClick`
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
### `ContractSelectionList` — Component
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
type ContractSelectionListProps = {
|
|
255
|
+
contracts: ContractData[];
|
|
256
|
+
isSingleContract: boolean;
|
|
257
|
+
selectedIds: Set<string>;
|
|
258
|
+
onKeyDown: (event: React.KeyboardEvent) => void; // for Cmd+A
|
|
259
|
+
onModifierClick: (id: string, event: React.MouseEvent) => void;
|
|
260
|
+
onShiftClick: (id: string, event: React.MouseEvent) => void;
|
|
261
|
+
};
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Renders the full list of `ContractListItem` components. Attaches `onKeyDown` for Cmd+A detection. Shows bulk toolbar when `selectedIds.size > 0` — toolbar includes remove button gated by `!isSingleContract`.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### `ContractListingLayout` — Layout
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
type ContractListingLayoutProps = {
|
|
272
|
+
data: {
|
|
273
|
+
orgUnit: OrgUnitBasicData;
|
|
274
|
+
contracts: ContractData[];
|
|
275
|
+
user: UserData | null;
|
|
276
|
+
};
|
|
277
|
+
loading?: boolean;
|
|
278
|
+
};
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Structure:
|
|
282
|
+
1. `GlobalLayout` wrapper
|
|
283
|
+
2. `OrgUnitDetailsNavbar` (reused from org-units)
|
|
284
|
+
3. `OrgUnitBreadcrumb` in `HeaderInside`
|
|
285
|
+
4. `ContractSelectionList` with `useContractSelection` wired in
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### Modified Files
|
|
290
|
+
|
|
291
|
+
| File | Change |
|
|
292
|
+
|------|--------|
|
|
293
|
+
| `src/features/contracts/components/index.ts` | Export `ContractListItem`, `ContractSelectionList` |
|
|
294
|
+
| `src/features/contracts/hooks/index.ts` | Export `useContractSelection` |
|
|
295
|
+
| `src/features/contracts/layouts/index.ts` | Export `ContractListingLayout` |
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Feature Specification: SUMA Phase 2 — Contract Switching & Profile (Contract Information)
|
|
2
|
+
|
|
3
|
+
**Feature Branch**: `feat/suma-switching`
|
|
4
|
+
|
|
5
|
+
**Created**: 2026-06-18
|
|
6
|
+
|
|
7
|
+
**Status**: Done
|
|
8
|
+
|
|
9
|
+
**Parent spec**: `specs/001-suma-phase-2/spec.md` (User Stories 3 & 4)
|
|
10
|
+
|
|
11
|
+
**Depends on**:
|
|
12
|
+
- `feat/suma-infra` — feature flag, routes, BFF service layer
|
|
13
|
+
- `feat/suma-listing` — Contract Listing page (provides the entry point for this flow)
|
|
14
|
+
|
|
15
|
+
> **Terminology**: In SUMA, **Contract Information** means the existing **Profile** page — route `/pvt/organization-account/profile/[orgUnitId]/[contractId]` (`buyerPortalRoutes.profileDetails`). After switching contracts, the buyer lands on Profile to operate under the selected contract. The separately registered `/contract-information` route is **not** the switch destination.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Business Context
|
|
20
|
+
|
|
21
|
+
### Problem
|
|
22
|
+
|
|
23
|
+
SUMA Phase 2 enables the same user to belong to multiple contracts (accounts). After the
|
|
24
|
+
Contract Listing page is in place, users need to switch into a specific contract's
|
|
25
|
+
context and manage it. Switching requires updating the session token via the BFF, then
|
|
26
|
+
landing on the Profile page (Contract Information) for that contract.
|
|
27
|
+
|
|
28
|
+
### SUMA Mental Model
|
|
29
|
+
|
|
30
|
+
Switching contracts updates the **active commercial context** for the session. A plain
|
|
31
|
+
click on a contract row:
|
|
32
|
+
|
|
33
|
+
1. Calls the BFF `switch-properties` endpoint (`changeContractToken`) with the target contract id.
|
|
34
|
+
2. Clears persisted host session state so SSR re-runs under the new cookie.
|
|
35
|
+
3. Full-navigates to the **Profile page** (`buyerPortalRoutes.profileDetails`) for that contract.
|
|
36
|
+
4. Broadcasts to other tabs so open listing pages refresh.
|
|
37
|
+
|
|
38
|
+
### Goals
|
|
39
|
+
|
|
40
|
+
- Enable click-through from the listing to the Profile page (Contract Information) for each contract.
|
|
41
|
+
- Show a loading overlay while the BFF switch and navigation are in progress.
|
|
42
|
+
- Surface an error toast on the listing when the switch fails (user stays on listing).
|
|
43
|
+
- Synchronize open tabs via `BroadcastChannel` so the listing refreshes when the user
|
|
44
|
+
switches or removes a contract in another tab.
|
|
45
|
+
|
|
46
|
+
### Out of Scope
|
|
47
|
+
|
|
48
|
+
- The `/contract-information` dedicated route as a switch destination (Profile is canonical).
|
|
49
|
+
- Propagate-to-children on remove (deferred).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## User Scenarios & Testing
|
|
54
|
+
|
|
55
|
+
### User Story 1 — Switch to Profile (Contract Information) (Priority: P1)
|
|
56
|
+
|
|
57
|
+
A buyer clicks a contract row in the listing to enter that contract's context.
|
|
58
|
+
|
|
59
|
+
**Acceptance Scenarios**:
|
|
60
|
+
|
|
61
|
+
1. **Given** the Contract Listing page is displayed, **When** the user clicks a contract row (no modifier key), **Then** a loading overlay appears, the BFF switch-properties call runs, and on success the browser navigates to the Profile page (`/profile/[orgUnitId]/[contractId]`) for that contract.
|
|
62
|
+
2. **Given** the BFF switch fails, **When** the request errors, **Then** the user remains on the Contract Listing and an error toast is shown.
|
|
63
|
+
3. **Given** the user clicks with Cmd/Ctrl, **Then** the multi-select toggle fires (no navigation) — existing listing behaviour is unchanged.
|
|
64
|
+
4. **Given** the switch succeeds, **When** the BroadcastChannel message is sent, **Then** all other open tabs showing the listing reload.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### User Story 2 — Profile Page (Contract Information) (Priority: P1)
|
|
69
|
+
|
|
70
|
+
The Profile page shows the contract's identity, basic details, and navigation under the new session context.
|
|
71
|
+
|
|
72
|
+
**Acceptance Scenarios**:
|
|
73
|
+
|
|
74
|
+
1. **Given** the user lands on `/pvt/organization-account/profile/[orgUnitId]/[contractId]` after a successful switch, **When** the page loads, **Then** the contract name and profile details are displayed under the selected contract context.
|
|
75
|
+
2. **Given** the user is on a SUMA org page with the sidebar visible, **When** they use the "Back to contracts" link, **Then** they are navigated back to the Contract Listing page.
|
|
76
|
+
3. **Given** the user clicks a contract-scoped tab (Addresses, Payment methods, etc.), **Then** the browser navigates to the corresponding existing buyer portal route for that contract.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### User Story 3 — Remove Contract (Priority: P1)
|
|
81
|
+
|
|
82
|
+
Contract removal is handled from the **Contract Listing** page (bulk toolbar and per-row dots menu). See `specs/suma-remove-contracts.md`.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### User Story 4 — Error Handling on Switch (Priority: P1)
|
|
87
|
+
|
|
88
|
+
When the BFF switch fails, the user stays on the listing with feedback.
|
|
89
|
+
|
|
90
|
+
**Acceptance Scenarios**:
|
|
91
|
+
|
|
92
|
+
1. **Given** the BFF switch-properties call fails, **When** the error is caught, **Then** the loading overlay clears and an error toast appears on the Contract Listing page.
|
|
93
|
+
2. **Given** the Contract Listing page mounts with `?error=contract-load-failed` (legacy redirect param), **Then** an auto-dismissing toast fires with the message "Failed to load contract information." and the query param is removed.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
### User Story 5 — Tab Synchronisation (Priority: P2)
|
|
98
|
+
|
|
99
|
+
State changes in one tab propagate to other open tabs.
|
|
100
|
+
|
|
101
|
+
**Acceptance Scenarios**:
|
|
102
|
+
|
|
103
|
+
1. **Given** the user is on the listing in Tab 1 and Tab 2 switches to a contract (Profile page), **Then** Tab 1 reloads to reflect the current state.
|
|
104
|
+
2. **Given** the user removes a contract in Tab 2, **Then** Tab 1's listing refreshes and the removed contract no longer appears.
|
|
105
|
+
3. **Given** the browser does not support `BroadcastChannel`, **Then** the app degrades gracefully — no crash, just no cross-tab sync.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### Key Scenarios
|
|
110
|
+
|
|
111
|
+
| Scenario | Pre-condition | Action | Expected result |
|
|
112
|
+
|----------|--------------|--------|----------------|
|
|
113
|
+
| **Happy path — switch** | Listing, 3 contracts | Click row | Loading overlay → BFF switch → Profile page loads |
|
|
114
|
+
| **Switch failure** | BFF returns error | Click row | Toast on listing; overlay clears; no navigation |
|
|
115
|
+
| **Tab sync — switch** | Listing open in Tab 1 | Tab 2 switches contract | Tab 1 reloads |
|
|
116
|
+
| **Tab sync — remove** | Listing open in Tab 1 | Tab 2 removes a contract | Tab 1 reloads; row gone |
|
|
117
|
+
| **Modifier click** | Listing page | Cmd+Click row | Multi-select toggled; no navigation |
|
|
118
|
+
| **Error toast not replayed** | Listing with `?error=…` | Reload page | No second toast |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Requirements
|
|
123
|
+
|
|
124
|
+
### Functional
|
|
125
|
+
|
|
126
|
+
- **FR-001**: Plain click on `ContractRow` calls `useSwitchContract` → BFF `changeContractToken` → navigates to `buyerPortalRoutes.profileDetails({ orgUnitId, contractId })`.
|
|
127
|
+
- **FR-002**: Cmd/Ctrl click and Shift+Click preserve existing multi-select behaviour (no switch).
|
|
128
|
+
- **FR-003**: On successful switch, fire `BroadcastChannel` message `{ type: 'contract-activated', contractId }`.
|
|
129
|
+
- **FR-004**: A loading overlay (`ContractSwitchOverlay`) MUST cover the row while the switch is in progress.
|
|
130
|
+
- **FR-005**: On switch failure, show an error toast on the listing; user MUST remain on the listing.
|
|
131
|
+
- **FR-006**: Profile page renders under the new session context after full navigation.
|
|
132
|
+
- **FR-007**: Listing reads `?error=contract-load-failed` on mount (legacy), shows toast, removes query param from URL.
|
|
133
|
+
- **FR-008**: `BroadcastChannel` listener in listing calls `router.reload()` on any incoming message.
|
|
134
|
+
- **FR-009**: `BroadcastChannel` usage is guarded by `typeof BroadcastChannel !== 'undefined'`.
|
|
135
|
+
|
|
136
|
+
### Non-functional
|
|
137
|
+
|
|
138
|
+
- Zero new TypeScript errors (pre-existing baseline errors excluded).
|
|
139
|
+
- Page must pass `make lint`.
|
|
140
|
+
- New hooks and pure functions must have unit tests.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Arch Decisions
|
|
145
|
+
|
|
146
|
+
### Decision 1 — BFF switch + Profile navigation
|
|
147
|
+
|
|
148
|
+
**Chosen approach**: Plain click on `ContractRow` calls `useSwitchContract`, which:
|
|
149
|
+
|
|
150
|
+
1. POSTs to BFF `switch-properties` via `changeContractToken(contractId)`.
|
|
151
|
+
2. Clears persisted host session state (`clearPersistedSessionState`).
|
|
152
|
+
3. Broadcasts `{ type: 'contract-activated', contractId }` to other tabs.
|
|
153
|
+
4. Full-navigates to `buyerPortalRoutes.profileDetails({ orgUnitId, contractId })`.
|
|
154
|
+
|
|
155
|
+
A loading overlay covers the row until navigation completes. On failure, the overlay clears and an error toast is shown on the listing.
|
|
156
|
+
|
|
157
|
+
**Rationale**: Switching contracts requires updating the session cookie so SSR and store context reflect the selected commercial agreement. The Profile page is the canonical Contract Information destination in the buyer portal.
|
|
158
|
+
|
|
159
|
+
**Failure path**: Toast on listing; previous contract context remains active.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### Decision 2 — BroadcastChannel for tab sync
|
|
164
|
+
|
|
165
|
+
**Chosen approach**: `useContractSwitchChannel` wraps `BroadcastChannel` with channel name `'buyer-portal-active-contract'`. Listing subscribes and calls `router.reload()` on any message.
|
|
166
|
+
|
|
167
|
+
**Message shapes**:
|
|
168
|
+
```typescript
|
|
169
|
+
type ChannelMessage = { type: 'contract-activated'; contractId: string };
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Who sends**: `useSwitchContract` after a successful BFF switch.
|
|
173
|
+
|
|
174
|
+
**Who listens**: `ContractListingLayout`.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Technical Contract
|
|
179
|
+
|
|
180
|
+
### Key implementation files
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
src/features/contracts/
|
|
184
|
+
├── hooks/
|
|
185
|
+
│ ├── useSwitchContract.ts # BFF switch + profile navigation
|
|
186
|
+
│ └── useContractSwitchChannel.ts # BroadcastChannel wrapper
|
|
187
|
+
├── components/
|
|
188
|
+
│ └── ContractSwitchOverlay/ # Loading overlay during switch
|
|
189
|
+
└── utils/
|
|
190
|
+
├── changeContractToken.ts # BFF switch-properties call
|
|
191
|
+
└── clearPersistedSessionState.ts # Clear host session after switch
|
|
192
|
+
|
|
193
|
+
src/pages/profile.tsx # Contract Information destination
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Modified files
|
|
197
|
+
|
|
198
|
+
| File | Change |
|
|
199
|
+
|------|--------|
|
|
200
|
+
| `ContractListItem.tsx` | Plain click → `useSwitchContract`; loading overlay; error toast on failure |
|
|
201
|
+
| `ContractListingLayout.tsx` | Subscribe to BroadcastChannel → `router.reload()`; legacy `?error` toast handling |
|