diff --git a/app/javascript/controllers/goal_projection_chart_controller.js b/app/javascript/controllers/goal_projection_chart_controller.js index 621022fa4..791c0904c 100644 --- a/app/javascript/controllers/goal_projection_chart_controller.js +++ b/app/javascript/controllers/goal_projection_chart_controller.js @@ -146,8 +146,6 @@ export default class extends Controller { ] : []; - const pendingPledgeAmount = data.pending_pledge_amount || 0; - const yMax = Math.max(targetAmount * 1.05, projectionEnd, requiredEnd, currentAmount, 1); const x = d3.scaleTime().domain([start, endDate]).range([margin.left, margin.left + innerWidth]); @@ -332,47 +330,15 @@ export default class extends Controller { .attr("text-anchor", "end") .attr("font-size", 12) .attr("fill", textSecondary) + .attr("paint-order", "stroke") + .attr("stroke", containerBg) + .attr("stroke-width", 4) + .attr("stroke-linejoin", "round") .text(labelText); } } } - if (pendingPledgeAmount > 0 && target) { - const willHit = projectionEnd >= targetAmount; - const pendingColor = willHit ? "var(--color-green-600)" : "var(--color-yellow-600)"; - const pendingTop = Math.min(yMax, currentAmount + pendingPledgeAmount); - svg - .append("line") - .attr("x1", x(today)) - .attr("x2", x(today)) - .attr("y1", y(currentAmount)) - .attr("y2", y(pendingTop)) - .attr("stroke", pendingColor) - .attr("stroke-width", 3) - .attr("stroke-linecap", "round") - .attr("opacity", 0.4); - - svg - .append("circle") - .attr("cx", x(today)) - .attr("cy", y(pendingTop)) - .attr("r", 5) - .attr("fill", containerBg) - .attr("stroke", pendingColor) - .attr("stroke-width", 2) - .attr("stroke-dasharray", "2 2"); - - if (innerWidth >= 320) { - svg - .append("text") - .attr("x", x(today) + 10) - .attr("y", y(pendingTop) + 4) - .attr("font-size", 12) - .attr("fill", textSecondary) - .text(`+ pending ${data.pending_pledge_label_short}`); - } - } - svg .append("line") .attr("x1", x(today)) diff --git a/app/models/goal.rb b/app/models/goal.rb index 80d03d918..dbc96e4df 100644 --- a/app/models/goal.rb +++ b/app/models/goal.rb @@ -188,7 +188,6 @@ class Goal < ApplicationRecord saved_series = series_values.map { |v| { date: v.date.to_s, value: v.value.amount.to_f } } earliest = series_values.first&.date || created_at.to_date - pending = open_pledges.sum(:amount).to_d target_amt = target_amount.to_d proj_end = projection_end_amount @@ -206,8 +205,6 @@ class Goal < ApplicationRecord required_monthly: monthly_target_amount.to_f, currency: currency, status: status.to_s, - pending_pledge_amount: pending.to_f, - pending_pledge_label_short: short_money(pending, currency), projection_end_value: proj_end.to_f, projection_end_label: Money.new(proj_end, currency).format(precision: 0), projection_shortfall_label: (target_amt > proj_end ? Money.new(target_amt - proj_end, currency).format(precision: 0) : nil) @@ -306,6 +303,34 @@ class Goal < ApplicationRecord end end + # Single-line state summary rendered between the header and the ring on + # the show page. Replaces the stacked catch-up alert + inline status pill; + # carries the same actionable copy without owning a CTA. Returns nil when + # the projection-side cards already convey state (paused / archived / + # completed / reached) so the callout doesn't double up. + def status_callout_context + return nil if paused? || archived? || completed? || status == :reached + + case status + when :behind + delta = catch_up_delta_money.amount + if delta.positive? + I18n.t("goals.show.status_callout.behind", + amount: catch_up_delta_money.format(precision: 0)) + else + I18n.t("goals.show.status_callout.behind_covered") + end + when :on_track + if target_date && pace.to_d.positive? + months = (remaining_amount.to_d / pace.to_d).ceil + I18n.t("goals.show.status_callout.on_track", + date: I18n.l(Date.current >> months.to_i, format: "%b %Y")) + end + when :no_target_date + I18n.t("goals.show.status_callout.no_target_date") + end + end + # Header copy under the goal title on show. Used to live as a multi-line # if/elsif block in show.html.erb. Keeps the view template free of date # math + i18n key picking. diff --git a/app/views/goals/_pending_pledge_banner.html.erb b/app/views/goals/_pending_pledge_banner.html.erb index 1e93a246f..5b716a5c9 100644 --- a/app/views/goals/_pending_pledge_banner.html.erb +++ b/app/views/goals/_pending_pledge_banner.html.erb @@ -5,29 +5,37 @@ body_key = pledge.kind_transfer? ? "goals.show.pending_pledge.body_transfer" : "goals.show.pending_pledge.body_manual" title = t("goals.show.pending_pledge.title", amount: amount_money.format(precision: 0), account: account.name, count: days_left) %> -<%= render DS::Alert.new(variant: "warning", title: title, live: :polite) do %> -
<%= t(body_key) %>
-<%= t("goals.show.pending_pledge.pledged_at", time_ago: time_ago_in_words(pledge.created_at)) %>
-+ <%= title %> + · <%= t("goals.show.pending_pledge.pledged_at", time_ago: time_ago_in_words(pledge.created_at)) %> +
+<%= t(body_key) %>
+<%= @goal.header_summary %>
+<%= @goal.header_summary %>
<% last_days = @goal.last_matched_pledge_days_ago %> <% unless last_days.nil? %><%= last_days.zero? ? t("goals.goal_card.footer_last_today") : t("goals.goal_card.footer_last_days", count: last_days) %>
<% end %> -- <%= t("goals.show.catch_up.body", avg: @goal.pace_money.format(precision: 0), required: Money.new(@goal.monthly_target_amount, @goal.currency).format(precision: 0)) %> -
-<%= t(".ring.to_go", amount: @goal.remaining_amount_money.format(precision: 0)) %>
<% end %> + <% unless @goal.completed? || @goal.status == :reached || @goal.paused? || @goal.archived? %> + <%# Single Record pledge entry point on the page. Pre-filled with the + catch-up delta when behind so accepting once funds the gap. %> + <% prefill_amount = @goal.status == :behind && @goal.catch_up_delta_money.amount.positive? ? @goal.catch_up_delta_money.amount.to_f : nil %> +<%= sanitize @goal.projection_summary %>
+ <% if @goal.status == :behind && @goal.monthly_target_amount %> ++ <%= t("goals.show.catch_up.body", avg: @goal.pace_money.format(precision: 0), required: Money.new(@goal.monthly_target_amount, @goal.currency).format(precision: 0)) %> +
+ <% end %>