* feat(accounts): add Gains / ROI chart view for investment accounts
Adds a fourth chart view to the account details page showing the
historical unrealized gains series (market value - cost basis per
holding, summed daily with LOCF and FX conversion), following the
same ChartSeriesBuilder/Series pipeline as the existing views.
Holdings without a usable cost basis (nil, or unlocked zero from
providers) contribute zero gain, matching Holding#avg_cost semantics.
* fix(accounts): carry cost basis forward over gap-filled holdings in gains series
Gap-filled holding rows (weekends, price-history gaps) are persisted
without cost_basis even though the position and basis are unchanged,
which zeroed the gains series on those dates. Look up the basis from
the latest snapshot that has a usable one instead of reading it from
the current row, so already-persisted gap-filled rows are handled too.
* docs(accounts): add method docstrings for gains chart view code
Satisfies the pre-merge docstring coverage check on methods added or
touched by the Gains / ROI feature.
* fix(accounts): sign converted amount like main indicator in gains view
Extracts the gains sign-prefix logic into a shared signed_format helper
so the family-currency converted amount shown on foreign-currency
accounts matches the main indicator (+€79.53 / +$85.00), and adds
component tests covering the positive, negative, non-gains and
foreign-currency formatting paths.
* test(accounts): cover FX conversion path in gains series
The gains_series tests only used USD holdings against a USD target,
leaving the exchange_rates LATERAL join untested. Adds a case with EUR
holdings converted to USD, including LOCF rate carry-forward.
---------
Co-authored-by: Antoine GUYON <agy@ibanfirst.com>
* feat(ds): elevate dropdown overlays and stabilize selection check gutter
Menus and popovers floated at the same elevation as inline cards
(shadow-border-xs), so dropdowns blended into the content beneath them.
Bump DS::Menu and DS::Popover panels to shadow-border-lg.
DS::MenuItem rendered its leading icon only when present, so a selection
check shifted the row's text out of alignment with the unselected rows.
Add a `selected:` param that reserves a fixed-width check gutter (check
when selected, empty otherwise) so row text stays aligned. Apply the same
reserved gutter to the bespoke category dropdown row, and add a
`selectable` menu preview.
* feat(dashboard): unify per-widget period selectors into one picker
The dashboard rendered three identical period <select>s (cashflow,
outflows, net worth), each writing the same global User#default_period
and full-reloading the page via turbo_frame "_top" — so changing one
changed all. Replace them with a single shared UI::PeriodPicker
(DS::Menu of period links) in a toolbar, and wrap the sections grid in a
"dashboard_sections" Turbo frame so a period change swaps only the
dashboard (no full-page reload). Reuse the same picker on the account
chart, removing its duplicate select.
* refactor(dashboard): use DS::MenuItem selected: gutter in period picker
Now that DS::MenuItem reserves a check gutter, the period picker passes
selected: instead of a conditional leading check icon, so the current
period's row stays aligned with the rest.
* fix(ds): expose menu selection via menuitemradio + aria-checked
Selectable DS::MenuItem rows conveyed selection only visually. Render them
as role="menuitemradio" with aria-checked so assistive tech gets the
selection state of single-select lists, merging the menu ARIA contract with
any caller-supplied aria. Addresses CodeRabbit review feedback.
* refactor(dashboard): drop redundant aria-current from period picker
DS::MenuItem now exposes selection via menuitemradio + aria-checked, so the
period picker no longer needs its own aria-current. Update the component
test to assert the new ARIA.
* fix(ds): include selectable roles in menu roving-focus query
DS::MenuItem selectable rows render as role=menuitemradio, but the menu
controller built its roving-focus list from [role=menuitem] only, leaving
single-select menus with no keyboard focus/arrow handling. Query the
menuitemradio/menuitemcheckbox roles too. Addresses Codex review feedback.
* fix(dashboard): keep non-picker links out of frame + fix custom-month picker
- turbo_frame_tag "dashboard_sections" now targets _top so ordinary links
inside the sections (e.g. Balance Sheet account links) navigate the page
instead of failing to resolve inside the frame.
- Period.current_month_for / last_month_for carry their semantic key for
custom-month families, so the picker shows the right label and checks the
right option instead of falling back to 30D.
Addresses Codex review feedback.
* fix(dashboard): announce selected period in picker trigger's accessible name
The static "Select time period" aria-label overrode the visible selected
label as the trigger's accessible name, so assistive tech kept announcing
the same name regardless of selection. Interpolate the selected short
label into the aria-label and pin it with a component test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>