Skip to content

feat(frontend/purchase-modal): full per-purchase breakdown + per-row skip checkboxes#325

Merged
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
feat/purchase-modal-details-checkboxes
May 6, 2026
Merged

feat(frontend/purchase-modal): full per-purchase breakdown + per-row skip checkboxes#325
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
feat/purchase-modal-details-checkboxes

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 6, 2026

Summary

  • Expands the Execute Purchase modal (openPurchaseModal) to show full per-purchase detail: Account, Service/Type, Region, Count, Upfront, Monthly Cost, Effective Savings, and Effective %.
  • Adds per-row "Include" checkboxes (all checked by default) with a select-all/deselect-all header checkbox so users can skip individual purchases without leaving the modal.
  • Live totals row (sticky <tfoot>) updates on every checkbox toggle; Execute button disabled with tooltip when zero rows selected.
  • Only checked rows are submitted — getPurchaseModalRecommendations() now filters by checked indices; no changes to app.ts execute path needed.
  • Modal commits section scrolls with sticky header/footer so Cancel/Execute buttons are always visible on long lists.

Modal shows monthly totals regardless of the page's cost-period selector — by design (the modal is a commit-decision context where monthly is canonical).

Coordination note

Parallel agents are implementing #317, #318, #319 in separate worktrees on the same base branch. This PR touches only the modal area (openPurchaseModal ~L2959, renderPurchaseModalRow ~L3267, new updatePurchaseModalTotals) and does not overlap with the column-map / render area that #317#319 touch. Minor rebase may be needed after one of them merges.

Implementation details

  • Reuses effectiveMonthlySavings / effectiveSavingsPct helpers (no re-derivation).
  • checkedPurchaseIndices: Set<number> tracks included rows; checkedPurchaseModalInitialised flag distinguishes "user cleared all" from "modal never opened".
  • clearPurchaseModalRecommendations() resets both, preserving existing close behaviour.
  • CSS in styles/modals.css: .purchase-modal-commits with max-height: 55vh; overflow-y: auto, sticky thead / tfoot.
  • 12 new unit tests + 1 updated test (removed "Purchase Summary" heading check that no longer applies).

Test plan

  • All 1527 existing tests pass (npm test from frontend/)
  • npm run build succeeds
  • New tests cover: all-selected default, per-row exclusion from submitted set, totals math, disabled-button state, select-all/deselect-all, column headers

Closes #320

Summary by CodeRabbit

  • New Features

    • Per-row "Include" checkboxes in the purchase modal with selective bulk purchase behavior.
    • Richer breakdown table (account, service/type, region, counts, costs, savings, term, payment) and live totals footer.
    • All rows default to selected on modal open.
    • New public control to reset/clear modal selections.
  • Tests

    • Expanded test coverage for per-row selection, select-all semantics, totals, detached popovers, and purchase modal workflows.
  • Style

    • New modal styles for sticky headers, totals footer, and full breakdown table.
  • Docs

    • Updated documentation to reflect new modal capabilities.

…skip checkboxes

Expands the single-bucket Execute Purchase modal (openPurchaseModal) to show
all per-purchase details needed for an informed commit decision, and adds
per-row checkboxes so users can exclude individual purchases without leaving
the modal.

Changes:
- New columns: Account, Service/Type, Region, Count, Upfront, Monthly Cost,
  Eff. Savings, Eff. % (reusing effectiveMonthlySavings / effectiveSavingsPct
  helpers — no re-derivation).
- Per-row "Include" checkbox (default checked) + select-all/deselect-all in
  the table header.
- Live totals row (tfoot) that updates on every checkbox toggle: total count,
  upfront, monthly cost, effective savings, weighted-average effective %.
- Execute Purchase button disabled (with tooltip) when zero rows selected.
- getPurchaseModalRecommendations() now filters by checkedPurchaseIndices so
  only checked rows are submitted — no changes to app.ts execute path needed.
- clearPurchaseModalRecommendations() resets checkbox state.
- CSS: .purchase-modal-commits has max-height + overflow-y:auto with sticky
  thead/tfoot so the table scrolls cleanly on long lists.
- Modal shows monthly totals regardless of the page's cost-period selector —
  by design (modal is a commit-decision context where monthly is canonical).
- 12 new unit tests covering: all-selected default, per-row exclusion, totals
  math, disabled-button state, select-all/deselect-all, column headers.

Closes #320
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

Per-row selection state and UI were added to the Purchase modal: rows are checked by default when the modal opens, users can toggle per-row Include checkboxes (with a select‑all header), totals update live, and only checked rows are returned by getPurchaseModalRecommendations. clearPurchaseModalRecommendations() resets modal selection state. Tests and CSS for the modal were added/updated.

Changes

Per-Row Purchase Selection & Modal Expansion

Layer / File(s) Summary
Selection State & API
frontend/src/recommendations.ts
Adds module-scoped checked-indices Set and an "initialized" flag; getPurchaseModalRecommendations filters to checked rows after modal init; clearPurchaseModalRecommendations() resets recommendations, checked indices, and init flag.
Modal Initialization
frontend/src/recommendations.ts
On opening the purchase modal all row indices are initialised as checked and the modal-initialized flag is set.
Modal Markup & Controls
frontend/src/recommendations.ts
Purchase modal expanded to include per-row "Include" checkboxes, select-all header checkbox, columns (Account, Service/Type, Region, Count, Upfront, Monthly Cost, Effective Savings, Effective %, Term, Payment), per-row Term/Payment wiring, event handlers to toggle inclusion and update totals, and an approval notice paragraph.
Totals Footer
frontend/src/recommendations.ts
Adds a sticky totals/footer row in the modal table that aggregates only checked rows and updates on interactions.
Styling
frontend/src/styles/modals.css
New CSS for commits panel, sticky table header/footer and totals row, include-label helpers. Note: stray numeric tokens were inserted at lines 152–154 inside .prereq-hint.
Tests
frontend/src/__tests__/recommendations.test.ts
Exports clearPurchaseModalRecommendations in tests; updates "shows purchase summary" test to assert breakdown table, "Include" header, "Totals" row, and approval note; adds a new "per-row skip checkboxes (issue #320)" test suite covering default checked state, filtering via getPurchaseModalRecommendations, per-row toggling, select-all/deselect-all semantics, Execute button enabled/disabled behavior, totals calculations, and clearPurchaseModalRecommendations clearing state.

Sequence Diagram

sequenceDiagram
    actor User
    participant Modal as Purchase Modal
    participant State as Selection State
    participant Totals as Totals Footer
    participant Executor as Execute Path

    User->>Modal: Open purchase modal
    Modal->>State: Initialize all rows checked
    State-->>Modal: Checked indices set
    Modal->>User: Render rows (all checked) + totals

    User->>Modal: Toggle per-row Include
    Modal->>State: Update checked indices
    State-->>Modal: Return updated checked set
    Modal->>Totals: Recalculate totals for checked rows
    Totals-->>Modal: New totals
    Modal->>User: Re-render totals & button state

    User->>Modal: Click Execute Purchase
    Modal->>State: getPurchaseModalRecommendations()
    State-->>Modal: Return only checked recommendations
    Modal->>Executor: Submit checked recommendations
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • LeanerCloud/CUDly#180: Modifies frontend/src/recommendations.ts and tests around bulk-purchase/fan-out and purchase-modal behavior.
  • LeanerCloud/CUDly#275: Changes selection semantics in recommendations; related to top-level vs. per-modal selection interactions.
  • LeanerCloud/CUDly#193: Overlaps on purchase-modal changes (Term/Payment dropdowns and modal wiring).

Suggested labels

priority/p2, severity/low

Poem

🐰
In the modal where choices once froze,
I hop and tick boxes row by row.
Totals dance when a check is spared,
Clear the state — no worries shared.
A rabbit cheers: fine-grained control grows.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main changes: adding per-row skip checkboxes and expanding the purchase modal to show full per-purchase breakdown, aligning with the core objectives.
Linked Issues check ✅ Passed The PR implementation addresses all coding objectives from issue #320: per-row checkboxes, live totals, disabled execute button when zero rows selected, checkbox state tracking, filtering submitted rows, scrollable modal with sticky header/footer, and corresponding unit tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the purchase modal enhancement and related test coverage. No unrelated modifications or out-of-scope features are present; stray CSS lines in modals.css appear to be accidental but are minimal.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/purchase-modal-details-checkboxes

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim cristim added priority/p1 Next up; this sprint severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/m Days type/feat New capability triaged Item has been triaged labels May 6, 2026
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 6, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/recommendations.ts`:
- Around line 3141-3152: The weighted-average denominator must match
effectiveSavingsPct's logic: when computing weightedEffSavingsNum/Den in the
loop (variables weightedEffSavingsNum, weightedEffSavingsDen, and the code that
computes amortized, effSav, onDemand), prefer rec.on_demand_cost when present
instead of reconstructing onDemand from rec.monthly_cost + rec.savings +
amortized; also include recs where rec.monthly_cost is null but
rec.on_demand_cost exists (so they aren't erroneously skipped). Update the
onDemand assignment to use rec.on_demand_cost ?? (rec.monthly_cost + rec.savings
+ amortized) and keep the term>0 guard (or mirror effectiveSavingsPct's
null-check) so the weighted average aligns with per-row effectiveSavingsPct and
effectiveMonthlySavings(rec).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f959c43-e0f3-43c7-8960-f132c10b2cf3

📥 Commits

Reviewing files that changed from the base of the PR and between 7032056 and 6399b6d.

📒 Files selected for processing (3)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts
  • frontend/src/styles/modals.css

Comment thread frontend/src/recommendations.ts Outdated
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 6, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/recommendations.ts (1)

3393-3409: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Term edits leave effective metrics stale in both row and totals.

After Line 3397 mutates live.term, the row’s Eff. Savings / Eff. % cells and the totals footer are not recomputed. Users can change term and still see pre-change economics until they toggle an Include checkbox.

💡 Suggested fix
   termSelect.addEventListener('change', () => {
     const live = currentPurchaseRecommendations[idx];
     if (!live) return;
     const newTerm = parseInt(termSelect.value, 10) === 3 ? 3 : 1;
     live.term = newTerm;
     // Rebuild this row's payment options for the new term; if current
     // payment is no longer supported, pick the first valid option and
     // mirror back to live state.
     rebuildPaymentOptions(
       paymentSelect,
       live.provider as CompatProvider,
       live.service,
       newTerm,
       (live.payment ?? '') as CompatPayment,
     );
     live.payment = paymentSelect.value;
+
+    // Refresh per-row computed metrics that depend on term.
+    effSavTd.textContent = formatCurrency(effectiveMonthlySavings(live));
+    const nextPct = effectiveSavingsPct(live);
+    effPctTd.classList.toggle('effective-pct-negative', nextPct !== null && nextPct < 0);
+    effPctTd.textContent = nextPct !== null ? `${nextPct.toFixed(1)}%` : '—';
+
+    // Keep totals/footer and execute-state in sync with edited term values.
+    const selectAllCb = document.getElementById('purchase-modal-select-all') as HTMLInputElement | null;
+    if (selectAllCb) updatePurchaseModalTotals(selectAllCb);
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/recommendations.ts` around lines 3393 - 3409, After mutating
live.term (the object from currentPurchaseRecommendations) you must recompute
and re-render that row's effective metrics and the totals footer; update the
change handler on termSelect so that after setting live.term (and after
rebuildPaymentOptions / live.payment assignment) it calls the same routines used
by the Include-toggle path to recalculate row economics and refresh aggregate
totals (i.e., invoke the functions that recompute a recommendation's effective
savings/percent and the function that updates the totals footer). Locate the
include-checkbox handler to find the exact helper names and call those helpers
(or extract them if needed) so the row cells and totals stay in sync when term
changes.
🧹 Nitpick comments (2)
frontend/src/__tests__/recommendations.test.ts (2)

1477-1488: ⚡ Quick win

Also assert the zero-selection guidance text, not only disabled.

Issue #320 calls out explicit guidance (“Select at least one purchase”). This test currently validates only button state, so the UX contract can regress unnoticed.

Proposed assertion addition
       const btn = document.getElementById('execute-purchase-btn') as HTMLButtonElement | null;
       expect(btn?.disabled).toBe(true);
+      // Pin the disabled-state guidance contract from issue `#320`.
+      expect(btn?.getAttribute('title') ?? document.getElementById('purchase-modal-disabled-hint')?.textContent ?? '')
+        .toContain('Select at least one purchase');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/__tests__/recommendations.test.ts` around lines 1477 - 1488, The
test should also assert that the zero-selection guidance text appears when all
rows are unchecked: after using openPurchaseModal([...]) and unchecking the
.purchase-modal-row-include checkboxes (and dispatching 'change'), in addition
to checking execute-purchase-btn.disabled === true, query the DOM for the
guidance element (e.g., text content matching "Select at least one purchase" or
the element that renders that message) and assert it is visible/present; update
the test case name/comment if needed to reflect that it verifies both disabled
state and guidance text.

1428-1614: ⚡ Quick win

Add a per-test modal-state reset in this describe block.

This suite mutates modal selection state repeatedly; adding a local reset makes test order irrelevant and prevents hidden coupling.

Proposed test-hardening patch
 describe('per-row skip checkboxes (issue `#320`)', () => {
+  beforeEach(() => {
+    clearPurchaseModalRecommendations();
+  });
+
   const makeRec = (id: string, opts: Partial<{
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/__tests__/recommendations.test.ts` around lines 1428 - 1614, The
tests in the "per-row skip checkboxes" suite mutate modal selection state across
tests; ensure each test resets modal state by calling
clearPurchaseModalRecommendations() (or an equivalent reset) before or after
openPurchaseModal() so tests are isolated; update the describe block to invoke
clearPurchaseModalRecommendations() in a beforeEach (or directly at start of
each test) to reset checkboxes, select-all state, and totals so functions like
openPurchaseModal, getPurchaseModalRecommendations, and
clearPurchaseModalRecommendations operate on a clean modal for every test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@frontend/src/recommendations.ts`:
- Around line 3393-3409: After mutating live.term (the object from
currentPurchaseRecommendations) you must recompute and re-render that row's
effective metrics and the totals footer; update the change handler on termSelect
so that after setting live.term (and after rebuildPaymentOptions / live.payment
assignment) it calls the same routines used by the Include-toggle path to
recalculate row economics and refresh aggregate totals (i.e., invoke the
functions that recompute a recommendation's effective savings/percent and the
function that updates the totals footer). Locate the include-checkbox handler to
find the exact helper names and call those helpers (or extract them if needed)
so the row cells and totals stay in sync when term changes.

---

Nitpick comments:
In `@frontend/src/__tests__/recommendations.test.ts`:
- Around line 1477-1488: The test should also assert that the zero-selection
guidance text appears when all rows are unchecked: after using
openPurchaseModal([...]) and unchecking the .purchase-modal-row-include
checkboxes (and dispatching 'change'), in addition to checking
execute-purchase-btn.disabled === true, query the DOM for the guidance element
(e.g., text content matching "Select at least one purchase" or the element that
renders that message) and assert it is visible/present; update the test case
name/comment if needed to reflect that it verifies both disabled state and
guidance text.
- Around line 1428-1614: The tests in the "per-row skip checkboxes" suite mutate
modal selection state across tests; ensure each test resets modal state by
calling clearPurchaseModalRecommendations() (or an equivalent reset) before or
after openPurchaseModal() so tests are isolated; update the describe block to
invoke clearPurchaseModalRecommendations() in a beforeEach (or directly at start
of each test) to reset checkboxes, select-all state, and totals so functions
like openPurchaseModal, getPurchaseModalRecommendations, and
clearPurchaseModalRecommendations operate on a clean modal for every test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0a8ecfb1-f044-4ab3-9c42-909985b49653

📥 Commits

Reviewing files that changed from the base of the PR and between 6399b6d and ad0331e.

📒 Files selected for processing (2)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts

@cristim cristim merged commit 9c796ac into feat/multicloud-web-frontend May 6, 2026
6 checks passed
@cristim cristim deleted the feat/purchase-modal-details-checkboxes branch May 6, 2026 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p1 Next up; this sprint severity/medium Moderate harm triaged Item has been triaged type/feat New capability urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant