diff --git a/app/javascript/controllers/goal_projection_chart_controller.js b/app/javascript/controllers/goal_projection_chart_controller.js index 8f157a1c1..5ce7f5c22 100644 --- a/app/javascript/controllers/goal_projection_chart_controller.js +++ b/app/javascript/controllers/goal_projection_chart_controller.js @@ -399,8 +399,16 @@ export default class extends Controller { } _fmtMoney(amount, currency) { - const symbol = currency === "EUR" ? "€" : currency === "GBP" ? "£" : "$"; - return `${symbol}${Math.round(amount).toLocaleString()}`; + try { + return new Intl.NumberFormat(undefined, { + style: "currency", + currency: currency || "USD", + maximumFractionDigits: 0, + }).format(amount); + } catch { + const symbol = currency === "EUR" ? "€" : currency === "GBP" ? "£" : "$"; + return `${symbol}${Math.round(amount).toLocaleString()}`; + } } _fmtMoneyShort(amount, currency) { diff --git a/app/javascript/controllers/goal_stepper_controller.js b/app/javascript/controllers/goal_stepper_controller.js index 7c3bf5bff..d92b20faa 100644 --- a/app/javascript/controllers/goal_stepper_controller.js +++ b/app/javascript/controllers/goal_stepper_controller.js @@ -26,7 +26,6 @@ export default class extends Controller { "initialContributionAccountSelect", "reviewName", "reviewSummary", - "reviewAccounts", "reviewSuggested", "footerLeftButton", "footerRightButton", @@ -42,6 +41,13 @@ export default class extends Controller { backLabel: { type: String, default: "Back" }, continueLabel: { type: String, default: "Continue" }, submitLabel: { type: String, default: "Create goal" }, + currency: { type: String, default: "USD" }, + summaryWithDate: { type: String, default: "{amount} by {date}" }, + summaryNoDate: { type: String, default: "{amount}" }, + accountCountOne: { type: String, default: "1 account" }, + accountCountOther: { type: String, default: "{count} accounts" }, + suggestedWithDate: { type: String, default: "Save {monthly}/mo across {accounts} to hit it on time." }, + suggestedNoDate: { type: String, default: "Set a target date to project a finish line." }, }; connect() { @@ -237,41 +243,49 @@ export default class extends Controller { const amount = amountInput?.value ? Number.parseFloat(amountInput.value) : 0; const dateInput = this.element.querySelector('input[type="date"][name="goal[target_date]"]'); const dateValue = dateInput?.value; + const checked = this.linkedAccountCheckboxTargets.filter((cb) => cb.checked); this.reviewNameTarget.textContent = name; if (this.hasReviewSummaryTarget) { - const currency = amountInput?.dataset?.currency || "$"; - const formattedAmount = amountInput?.value ? `${currency}${amount.toLocaleString()}` : "—"; - this.reviewSummaryTarget.textContent = dateValue - ? `${formattedAmount} by ${this.#formatDate(dateValue)}` - : formattedAmount; - } - - if (this.hasReviewAccountsTarget) { - const checked = this.linkedAccountCheckboxTargets.filter((cb) => cb.checked); - const total = checked.reduce( - (sum, cb) => sum + Number.parseFloat(cb.dataset.accountBalance || 0), - 0, - ); - this.reviewAccountsTarget.textContent = checked.length - ? `${checked.length} ${checked.length === 1 ? "account" : "accounts"} · $${total.toLocaleString()} balance` - : "—"; + const formattedAmount = amount > 0 ? this.#money(amount) : "—"; + const template = dateValue ? this.summaryWithDateValue : this.summaryNoDateValue; + this.reviewSummaryTarget.textContent = template + .replace("{amount}", formattedAmount) + .replace("{date}", dateValue ? this.#formatDate(dateValue) : ""); } if (this.hasReviewSuggestedTarget) { const months = dateValue ? this.#monthsBetween(new Date(), new Date(dateValue)) : 0; - if (amount > 0 && months > 0) { + const accountLabel = checked.length === 1 + ? this.accountCountOneValue + : this.accountCountOtherValue.replace("{count}", checked.length.toString()); + + if (amount > 0 && months > 0 && checked.length > 0) { const perMonth = Math.ceil(amount / months); - this.reviewSuggestedTarget.textContent = `$${perMonth.toLocaleString()}/mo over ${Math.max(1, Math.round(months))} months`; - } else if (amount > 0) { - this.reviewSuggestedTarget.textContent = `$${amount.toLocaleString()} (no target date)`; + this.reviewSuggestedTarget.textContent = this.suggestedWithDateValue + .replace("{monthly}", this.#money(perMonth)) + .replace("{accounts}", accountLabel); + } else if (amount > 0 && checked.length > 0) { + this.reviewSuggestedTarget.textContent = this.suggestedNoDateValue; } else { this.reviewSuggestedTarget.textContent = "—"; } } } + #money(value) { + try { + return new Intl.NumberFormat(undefined, { + style: "currency", + currency: this.currencyValue || "USD", + maximumFractionDigits: 0, + }).format(value); + } catch { + return `${this.currencyValue || "$"}${Math.round(value).toLocaleString()}`; + } + } + #monthsBetween(from, to) { return (to - from) / (1000 * 60 * 60 * 24 * 30.44); } diff --git a/app/views/goals/_form_stepper.html.erb b/app/views/goals/_form_stepper.html.erb index b792bb475..f1df2e6c6 100644 --- a/app/views/goals/_form_stepper.html.erb +++ b/app/views/goals/_form_stepper.html.erb @@ -1,6 +1,13 @@ <%# locals: (goal:, linkable_accounts:) %> -
+
" + data-goal-stepper-summary-no-date-value="<%= t("goals.form_stepper.step2.review.summary_no_date") %>" + data-goal-stepper-account-count-one-value="<%= t("goals.form_stepper.step2.review.account_count.one") %>" + data-goal-stepper-account-count-other-value="<%= t("goals.form_stepper.step2.review.account_count.other") %>" + data-goal-stepper-suggested-with-date-value="<%= t("goals.form_stepper.step2.review.suggested_with_date") %>" + data-goal-stepper-suggested-no-date-value="<%= t("goals.form_stepper.step2.review.suggested_no_date") %>"> <% if goal.errors[:base].any? %> <%= render "shared/form_errors", model: goal %> <% end %> @@ -106,7 +113,7 @@

<%= t("goals.form_stepper.step2.subheading") %>

-
+
<%= render DS::FilledIcon.new(variant: :container, icon: "target", size: "lg", rounded: false) %>
@@ -115,15 +122,7 @@
-
- <%= t("goals.form_stepper.step2.funding_accounts") %> - -
- -
- <%= t("goals.form_stepper.step2.suggested_monthly") %> - -
+

diff --git a/config/locales/views/goals/en.yml b/config/locales/views/goals/en.yml index c40ecde16..4a661bb0d 100644 --- a/config/locales/views/goals/en.yml +++ b/config/locales/views/goals/en.yml @@ -258,10 +258,16 @@ en: label: Review & start heading: Looks good? subheading: Review your goal and add an optional starting contribution. - funding_accounts: Funding accounts - suggested_monthly: Suggested monthly add_initial_contribution: Add an initial contribution add_initial_contribution_sub: Optional · jumpstart this goal with funds you've already set aside. initial_amount: Amount initial_account: From account select_account: Select an account + review: + summary_with_date: "{amount} by {date}" + summary_no_date: "{amount}" + account_count: + one: 1 account + other: "{count} accounts" + suggested_with_date: "Save {monthly}/mo across {accounts} to hit it on time." + suggested_no_date: Set a target date to project a finish line.