Repository navigation
fix(frontend): one money formatter and placeholder, right-align money - #1376
Conversation
…ht-aligned money columns
Money was printed three ways: formatCurrency (separators), the cost
period cells' `$${x.toFixed(n)}` (so a yearly figure read "$123456"),
and savings-history's private helper. Absent values used "--" (from
formatCurrency and about a dozen literals), "—" and, in the Purchases
tables, "-"; the empty Home showed "--" in savings green next to a bold
"—". All money columns were left-aligned, so digits did not line up.
- utils.ts: export NO_VALUE ("—"); formatCurrency returns it for
null/NaN/Infinity.
- recommendations.ts: formatCostForPeriod and formatScaledRange go
through formatCurrency with the per-period precision, so every period
groups thousands.
- dashboard.ts, history.ts, plans.ts, recommendations.ts: every "--" /
"-" placeholder becomes NO_VALUE; an absent figure is no longer
styled as a saving.
- tables.css: th.num/td.num right-align with tabular-nums; the
Opportunities numeric columns (by COLUMN_DEFS kind) and the Purchase
History and approval queue count/money columns use it.
- index.html: the Purchases savings stat cards start at "—", not
"$0.00".
The savings-history module's own formatter is left to LeanerCloud#171/LeanerCloud#749.
Refs LeanerCloud#739
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 35 billable files and costs up to $8.75. Or wait 59 minutes for your next included review. View limit details
Comment |
Conflicts (all in frontend/src/history.ts): - Purchase History row: kept main's escaped count and upfrontCell() (LeanerCloud#1364 amortized upfront) and this branch's right-aligned `num` classes on count, monthly and savings. - Approval queue placeholders: kept this branch's NO_VALUE for account, term and payment; dropped the branch's local `const amortize` because main hoisted the same declaration above the row map. - Approval queue row: same combination as the history row. - upfrontCell() (main, merged cleanly) now emits class `num` on both its plain and "(in monthly)" variants, so the upfront column stays right-aligned like the other money columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since this branch was cut, main added placeholders that bypass NO_VALUE: - plans.ts formatRampProgress (LeanerCloud#1327) returned "--" for a missing step count; it now returns NO_VALUE like the other plan-card fields. - trends.ts (new Trends sub-tab, LeanerCloud#1317) showed "--" on its Latest and Average savings stat cards; they now use NO_VALUE, matching the monthly table, whose null cells come from formatCurrency and already render it. - trends.test.ts, trends.spec.ts, plans.test.ts and nullable-purchase-cost.spec.ts expected the old "--"/"-" text from formatCurrency(null) and the muted monthly cell; they now expect "—". Refs LeanerCloud#739 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/history.ts (Purchase History and approval-queue rows): kept main's no-commitments handling (LeanerCloud#1329: hasNoCommitments, dashOr, NO_RECS_HTML, termCell) and this branch's num classes on the count/upfront/monthly/savings cells and NO_VALUE for the missing monthly cost, created-by, account and payment cells. The upfront placeholder for an empty row also gets class num, matching upfrontCell. - frontend/src/recommendations.ts (sortable header): kept main's LeanerCloud#755 header structure (plain <th> with aria-sort, sort-btn and filter button siblings) and added this branch's num class for numeric columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… main added Main's LeanerCloud#1329 (no-commitments rows) added a muted "-" placeholder (MUTED_DASH) for Purchase History and approval-queue cells and a "-" Term column-filter label for a 0 term. They now use NO_VALUE ("—"), like the rest of the absent values on these tables, so a no-commitments row no longer mixes "-" with "—". purchases-placeholders-181.test.ts expects "—". Refs LeanerCloud#739 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
|
Local verification at
|
|
Marked ready and merged by IAmJSD's agent, at IAmJSD's request (IAmJSD reviewed the before/after screenshots; cristim is traveling), after the independent review and local verification above, all at |
Conflicts: - frontend/src/modules/chart-utils.ts: import block only; kept this branch's chart.js import (for upsertChart) and main's formatCurrency import (LeanerCloud#1376). - frontend/src/dashboard.ts: chart-utils import line only; import both main's formatSparseTrendMessage/MIN_TREND_POINTS (LeanerCloud#1334) and this branch's upsertChart. The sparse-data early return from main sits before the upsertChart call and destroys the chart like the empty state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…urrency Main's LeanerCloud#1376 (Refs LeanerCloud#739) changed formatCurrency's absent marker to NO_VALUE ("—"); formatCompactCurrency still hardcoded "--". Delegate null/NaN/Infinity to formatCurrency so the two cannot drift again. Refs LeanerCloud#749 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/dashboard.ts: import block only. Kept this branch's lib/upcoming-scope import and took main's widened chart-utils import (formatSparseTrendMessage, MIN_TREND_POINTS from LeanerCloud#1334). renderUpcomingPurchases merged cleanly with main's LeanerCloud#1376 formatter changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/dashboard.ts (imports): kept this branch's lib/kpi-tile import and took main's widened chart-utils import (formatSparseTrendMessage, MIN_TREND_POINTS from LeanerCloud#1334). - frontend/src/dashboard.ts (renderDashboardSummary): this branch replaced the hand-built tile loop with buildKpiTile; main (LeanerCloud#1376, LeanerCloud#739) added a guard to that loop so an absent NO_VALUE figure is not styled as a saving. Kept buildKpiTile and moved main's guard into it (lib/kpi-tile.ts: valueSavings && value !== NO_VALUE), so Home keeps main's behaviour. Because Opportunities uses the same builder, its savings tile showing NO_VALUE is now neutral too, matching LeanerCloud#739's intent. Added a kpi-tile.test.ts case for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/history.ts: utils import. Main (LeanerCloud#1376) added NO_VALUE to the utils import; this branch added the historyConfirmBody import. Kept both. - frontend/src/__tests__/ladder-timeline.test.ts: both sides appended a test at the same spot. Kept this branch's refreshLadderTimeline test (LeanerCloud#728) and main's planned-purchases layout and UTC date filter describe block (LeanerCloud#1387, issue LeanerCloud#753) one after the other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- frontend/src/dashboard.ts:
- imports: kept this branch's filterByEnabledProviders import and all of
main's new imports (upcoming-scope LeanerCloud#1365, sparse-trend helpers LeanerCloud#1334,
chart-a11y LeanerCloud#1381, first-run empty state LeanerCloud#1389).
- loadDashboard Promise.allSettled: combined both sides into five
fetches; this branch's loadEnabledProviders() (LeanerCloud#180) plus main's
listAccountsMinimal() for the first-run onboarding card (LeanerCloud#1389).
- call site: kept renderDashboardSummary(summaryData, recs, recsCount)
and main's renderHomeOnboarding(...). The onboarding check uses the
Enabled-Providers-filtered recs, matching the Opportunities first-run
check, which also runs on the preference-filtered list.
- helpers: kept both loadEnabledProviders() and main's
renderHomeOnboarding(); renderDashboardSummary keeps this branch's
recsCount parameter.
- savings tile detail: this branch's filtered count with main's
NO_VALUE placeholder (LeanerCloud#1376): recsCount ?? total_recommendations ??
NO_VALUE; Active Commitments takes main's NO_VALUE.
- frontend/src/recommendations.ts: import block; main's NO_VALUE added to
the utils import, kept this branch's enabled-providers import.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… savings tile Main's LeanerCloud#1376 replaced the '--' placeholder with NO_VALUE (an em dash). This branch's LeanerCloud#180 fallback test still expected '--' for the savings tile value when the recommendations fetch fails. Refs LeanerCloud#180 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1385) * feat(frontend): add formatCompactCurrency for chart axes Chart axes need short labels ("$1.5K", "$12.35K", "$1.23M"). The savings chart built them by hand with `$${(v / 1000).toFixed(1)}K`. Add one shared helper on Intl.NumberFormat's compact notation, pinned to en-US like formatCurrency and with the same absent-value marker, so the chart can drop its own formatting (issue #749). Refs #749 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): plot one savings series on one axis with shared formatters The Purchases savings chart drew period savings (left axis) and cumulative savings (right axis) in one plot, so the points where the lines crossed meant nothing. Its ticks read "$1500.00", and the module had its own formatCurrency ("$1.50K") with the same name as the shared helper in utils.ts, so the stat cards formatted money differently from the rest of the Purchases page. - Period savings are bars on a single y-axis with compact ticks (formatCompactCurrency: "$1.5K"). The running total is a separate view, chosen with a new Show: Savings per period / Cumulative savings select (#savings-series), on the same single axis. - Tooltips and the Period / Avg / Peak stat cards use the shared formatCurrency with cents ("$12,840.00", "$428.00/mo"); the local formatCurrency is removed. - The x-axis keeps its period-start anchor (QA 2.2) with the bar chart's default offset turned off. Refs #749 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): share formatCurrency's absent marker in formatCompactCurrency Main's #1376 (Refs #739) changed formatCurrency's absent marker to NO_VALUE ("—"); formatCompactCurrency still hardcoded "--". Delegate null/NaN/Infinity to formatCurrency so the two cannot drift again. Refs #749 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#1328) * fix(frontend): make Home and Opportunities agree on Potential Monthly Savings Both cards sum the same per-cell range, but over different rows. Opportunities drops recommendations for providers switched off in Settings -> Enabled Providers (#463); Home summed every provider, so with a provider disabled the two same-named KPIs disagreed. Home's "N recommendations" line also came from the backend total, which ignores that setting. Home now reads the config and applies the same filter through a shared helper (lib/enabled-providers.ts), and counts the rows it summed. A config fetch failure falls back to no filter, as on Opportunities. When column filters narrow the Opportunities list, its savings card now says "Filtered: X of Y recommendations", so a deliberately narrower scope is labeled instead of looking like a disagreement. Refs #180 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): label the Opportunities savings card when a filter is pushed to the server The "Filtered: X of Y" line only appeared when the visible count was below the loaded count. A single-value service or region filter is sent to the server (#162), so the loaded list is already narrowed, the two counts match, and the card showed a smaller total than Home with no label: the #180 symptom on every load after the first. The card now decides "filtered" from the active column filters and from the filters the last fetch pushed to the server. When the loaded list was narrowed server-side the unfiltered total isn't known, so the line names the filter instead of inventing a total, e.g. "Filtered: 6 recommendations (Service: ec2)". The e2e recs mock now applies service/region like the backend does. Refs #180 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(frontend): expect the em dash placeholder on Home's failed-fetch savings tile Main's #1376 replaced the '--' placeholder with NO_VALUE (an em dash). This branch's #180 fallback test still expected '--' for the savings tile value when the recommendations fetch fails. Refs #180 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Money was printed three ways (with the cost period set to yearly, Opportunities showed "$123456" with no separators), absent values used
--,—and-depending on the screen (an empty Home mixed a green--with a bold—), and every money column was left-aligned so digits didn't line up. This PR routes the Opportunities period figures throughformatCurrency, uses one—placeholder for absent values, and right-aligns count and money columns with tabular digits on Opportunities, Purchase History and the approval queue.Refs #739
Partial:
modules/savings-history.ts(its private compact formatter and its--) is left to #749, which owns that code, and right-alignment is not yet applied to the Inventory, Plans and RI Exchange tables.What changed
frontend/src/utils.ts: new exportedNO_VALUE(—);formatCurrencyreturns it fornull/NaN/Infinity.frontend/src/recommendations.ts: a smallformatScaledhelper sendsformatCostForPeriodandformatScaledRangethroughformatCurrencywith the existing per-period precision (yearly 0, daily/monthly 2 and 0, hourly 4), so every period groups thousands. Opportunities headers and cells forCOLUMN_DEFSkindnumeric(count, savings, upfront, monthly, on-demand, effective %) get classnum.dashboard.ts,history.ts,plans.ts,recommendations.ts,approval-details.ts: every--and-placeholder becomesNO_VALUE, and an absent KPI value (Home savings, Purchases summary cards) no longer gets the greensavingsclass.frontend/src/styles/tables.css:th.num, td.num { text-align: right; font-variant-numeric: tabular-nums }; the Purchase History and approval-queue count/money columns use it.frontend/src/index.html: the Purchases savings stat cards start at—instead of "$0.00".__tests__/money-format.test.tsandtests-e2e/money-columns.spec.ts, new assertions inhistory.test.tsanddashboard.test.ts, about 40 expectations updated from--/-to—, andNO_VALUEadded to 18 manualutilsmocks.Why this approach
formatCurrencystays the single formatter instead of adding the issue's suggestedformatMoney: 28 modules already import it, and en-UStoLocaleStringalready gives the same output asIntl.NumberFormat.th/td, because many text columns must stay left-aligned.Screenshots
Before is main (
a14dc8f2), after is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API.--,--, bold—, green--and-- recommendations. After: one neutral—in every tile, with no savings green on absent values.-for a missing monthly cost. After: Count/Upfront/Monthly/Savings right-aligned, and the missing monthly cost shows—.Verification
Run on this branch's head
e50b553bon top of maina14dc8f2:--randomizeon money-format/history/utils/plans: 293 passed. A randomized run includingdashboard.test.tsandrecommendations.test.tsfailed 14 tests, and the same 14 fail with the same seed (242556410) on untouched main, so that order dependence predates this branch. New tests:money-format.test.ts(NO_VALUE, separators in every period with the real utils), ahistory.test.tsassertion for which columns carrynum, adashboard.test.tsassertion that an absent savings KPI is not green, and a history banner assertion with no.value.savings.history-action-confirmation.spec.ts:177, a revoke refresh race) passed 14/14 when re-run alone and doesn't touch the changed code. The newmoney-columns.spec.tschecks computedtext-align: rightandtabular-numson money cells and headers, that text columns stay left, and the yearly "$123,456".test-git-secrets-allowlist.sh: 137 pass, 0 fail.Notes for review
savings-history.tsis untouched because chore(frontend): rename savings-history.ts's private formatCurrency to stop shadowing utils.ts's #171 and fix(frontend): purchases savings chart uses two y-axes and shadows formatCurrency #749 own that code, so the Purchases savings stat cards still format differently from the history cards until fix(frontend): purchases savings chart uses two y-axes and shadows formatCurrency #749 lands.—/mo committedand— recommendationsfor absent fields; right-aligned headers include their filter buttons.formatCostForPeriod(null)was already—, so the Opportunities null cells don't change.Overlaps
savings-history.tsand the same Purchases stat cards.recommendations.tsandapproval-details.ts, in different hunks.history-approval-queue,history-approve-button,history-cancel-button,history-cancel-permissions) andindex.html: expect test-file conflicts.history.ts,tables.css,index.htmland the history-truncation-notice tests.--fromformatCurrencywill conflict semantically, and other branches' history/dashboard test mocks may needNO_VALUEonce this merges.Those PRs will be opened alongside this one and merged in sequence, with conflicts resolved on the later branch.
Note for the UI owner
This changes UI code that @cristim owns. IAmJSD reviewed the before/after screenshots and asked for it to be merged; any design point you disagree with can be a follow-up.
🤖 Generated with Claude Code