mirror of
https://github.com/we-promise/sure.git
synced 2026-05-30 07:49:01 +00:00
fix(goals): address AI review on PR #1798 (CodeRabbit + Codex)
Correctness: - GoalPledge#matches? rejects outflows on transfer pledges so a +$200 purchase no longer satisfies a $200 deposit pledge after .abs - GoalsController#sync_linked_accounts! saves through the goal so currency/depository/family validations actually run on update - AlreadyClaimedError replaces empty RecordInvalid in resolve_with! and reconciler rescues the dedicated class - SweepExpiredGoalPledgesJob wraps each expire! in a per-record rescue - Assistant::Function::CreateGoal disambiguates duplicate account names and returns an absolute URL via mailer host config - Family#savings_inflow_velocity defensively scopes from the family's accounts (was Account.joins(:goal_accounts).where(goal_id: ...)) - GoalPledgesController#set_goal preloads linked_accounts + providers to drop the N+1 on any_connected_account? - Stepper subtitle update walks to the enclosing dialog before querySelector so two stepper instances don't fight over one header - categories/_form.html.erb data-action targets color-icon-picker, not the non-existent "category" controller UX / visual: - Projection chart drops preserveAspectRatio="none" and pins endDate at today for past-due goals so the today marker stays in-domain - _color_picker / categories form swap non-standard border-1 for border - Goals index search input uses ring-alpha-black-100 (was raw gray-500) Refactors: - Goal#header_summary extracts the multi-line ERB header block - Goal#catch_up_delta_money sums open_pledges in SQL - Goal#projection_summary uses I18n.l for the on-track month label - Account#default_pledge_kind moves the manual/transfer decision out of GoalPledgesController - GoalPledge::Reconciler iterates ordered (created_at, id) so first-claim wins is deterministic under non-sequential PKs - Goals::FundingAccountsBreakdownComponent + Goals::AccountStackComponent use clamp(0..) instead of Float::INFINITY / [x, 0].max - Goals::StatusPillComponent#label provides a titleize fallback - Goal projection chart skips the redundant initial _draw and reuses the snapped point in the past branch (no double-bisect) - Goal pledge preview drops maximumFractionDigits: 0 so USD/EUR show cents while JPY/KRW stay whole-unit - Demo generator captures the Wedding fund goal in the seed loop instead of looking it up by hardcoded name Tests: - GoalPledgeTest: outflow rejection - GoalsControllerTest: cross-currency attachment rejected on update - SweepExpiredGoalPledgesJobTest: cancelled coverage + per-record rescue - GoalTest: pledge_action_label_key flips to manual_save without an unconditional guard
This commit is contained in:
@@ -14,15 +14,16 @@ export default class extends Controller {
|
||||
static values = { data: Object, ariaLabel: String, ariaDescription: String };
|
||||
|
||||
connect() {
|
||||
this._draw();
|
||||
this._resize = this._draw.bind(this);
|
||||
window.addEventListener("resize", this._resize);
|
||||
// Container may have 0 width on initial connect (Turbo restoration,
|
||||
// hidden parent, etc). Re-draw whenever the box settles into a real
|
||||
// size.
|
||||
// size. The first observer callback also performs the initial paint.
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
this._observer = new ResizeObserver(() => this._draw());
|
||||
this._observer.observe(this.element);
|
||||
} else {
|
||||
this._draw();
|
||||
}
|
||||
// Repaint when the user toggles theme so SVG attributes (which bake
|
||||
// light/dark hex values at draw time) follow data-theme. Lives here
|
||||
@@ -87,7 +88,11 @@ export default class extends Controller {
|
||||
const currentAmount = data.current_amount || 0;
|
||||
const avgMonthly = data.avg_monthly || 0;
|
||||
|
||||
const endDate = target || new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||
// Past-due goals: pin endDate at today so the "today" marker stays inside
|
||||
// the x-domain instead of clipping right at the edge.
|
||||
const endDate = target
|
||||
? new Date(Math.max(target.getTime(), today.getTime()))
|
||||
: new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
// Drop any same-day-or-later points from the balance series: we own the
|
||||
// endpoint with `currentAmount` (live `linked_accounts.sum(:balance)`)
|
||||
@@ -144,8 +149,7 @@ export default class extends Controller {
|
||||
.append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.attr("viewBox", `0 0 ${width} ${height}`)
|
||||
.attr("preserveAspectRatio", "none");
|
||||
.attr("viewBox", `0 0 ${width} ${height}`);
|
||||
|
||||
// Drop the <title> child; browsers render it as a native hover tooltip
|
||||
// that fights with our own crosshair tooltip. aria-label gives the same
|
||||
@@ -502,12 +506,10 @@ export default class extends Controller {
|
||||
hoverSavedDot.style("display", "none");
|
||||
lines.push(`Projected: ${this._fmtMoney(projValue, data.currency)}`);
|
||||
} else {
|
||||
// Saved segment: snap saved dot to the nearest contribution; no
|
||||
// projection dot in the past.
|
||||
const i = bisectDate(savedSeries, hoverDate);
|
||||
const a = savedSeries[Math.max(0, i - 1)];
|
||||
const b = savedSeries[Math.min(savedSeries.length - 1, i)];
|
||||
const savedPoint = !a ? b : !b ? a : (hoverDate - a.date < b.date - hoverDate ? a : b);
|
||||
// Saved segment: hoverDate is already snapped to nearest savedSeries
|
||||
// entry above, so reuse that entry directly instead of running
|
||||
// bisectDate a second time.
|
||||
const savedPoint = savedSeries.find((p) => p.date.getTime() === hoverDate.getTime()) || savedSeries[savedSeries.length - 1];
|
||||
hoverSavedDot.attr("cx", x(savedPoint.date)).attr("cy", y(savedPoint.value)).style("display", null);
|
||||
hoverProjDot.style("display", "none");
|
||||
lines.push(`Saved: ${this._fmtMoney(savedPoint.value, data.currency)}`);
|
||||
|
||||
Reference in New Issue
Block a user