fix(savings_goals): neutral ring percent, chart start vertical-line, contribution select wrapper, deterministic account colors

Ring percentage no longer takes the warning yellow tint when behind —
the colored ring stroke + status pill + catch-up alert already signal
the state, doubling it on the percent number was noise. Reached stays
green (celebratory), everything else uses text-primary (white/dark).

Chart vertical line at the left edge was the (start_date, $0) point
the controller prepended to the saved series. When start_date equals
the first contribution date (now common after the earlier earliest-
contribution fix), this drew a vertical jump from $0 to first
contribution at x=start. Skip the prepend when there's no temporal
gap so the line starts at the first real point.

Add Contribution modal — wrap the source-account select in the styled
form-field via f.select instead of label_tag + bare select_tag. Match
the rest of Sure's form controls. Also pass hide_currency on the
amount field so single-currency families don't see a redundant USD
dropdown.

Account avatar colors — replace Ruby String#hash (randomized per
process by Ruby for DoS protection) with a deterministic MD5-based
pick from Savings::GoalAvatarComponent::PALETTE. Same account name
now resolves to the same color across processes and across
components. Apply via a new Savings::GoalAvatarComponent.color_for
helper used by both the form stepper account list and the goal-card
AccountStackComponent (which was hardcoding blue-500 for every avatar
in the stack, hence Chase + Ally looking identical on the wedding
card).
This commit is contained in:
Guillem Arias
2026-05-11 16:58:17 +02:00
parent 6254a02602
commit 093831a6e5
7 changed files with 32 additions and 19 deletions

View File

@@ -51,10 +51,17 @@ export default class extends Controller {
const endDate = target || new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
const savedSeries = [{ date: start, value: 0 }].concat(
(data.saved_series || []).map((p) => ({ date: new Date(p.date), value: p.value })),
);
if (savedSeries[savedSeries.length - 1].date < today) {
const rawSavedSeries = (data.saved_series || []).map((p) => ({ date: new Date(p.date), value: p.value }));
const firstContribDate = rawSavedSeries[0]?.date;
const savedSeries = [];
// Only seed a (start, 0) point when start_date predates the first
// contribution. Otherwise the line draws a vertical jump up at the
// chart's left edge.
if (!firstContribDate || firstContribDate.getTime() > start.getTime()) {
savedSeries.push({ date: start, value: 0 });
}
savedSeries.push(...rawSavedSeries);
if (savedSeries.length && savedSeries[savedSeries.length - 1].date < today) {
savedSeries.push({ date: today, value: currentAmount });
}