Files
sure/app/views/goals/_form.html.erb
Guillem Arias fa4b1c5698 fix(goals): drop new-goal stepper, unify create + edit form
The 2-step stepper on the create modal carried a review step whose only
real signal was a derived "Save $X/mo to hit it on time" hint. Name,
amount, and date are all visible in step 1, so the review step was
re-displaying form values the user just typed.

Collapses both flows into a single panel:

- `_form_stepper.html.erb` + `_form_edit.html.erb` → single
  `_form.html.erb` driven by `goal.persisted?` for URL / method /
  submit label.
- `goal_stepper_controller.js` → `goal_form_controller.js`. Drops the
  step1Panel / step2Panel / step1Indicator / step2Indicator /
  step1Circle / step2Circle / stepperLine / reviewName / reviewSummary
  / reviewSuggested / footerLeftButton / footerRightButton / submitButton
  target plumbing and the next / back / blockEnter / updateStepperState
  / updateFooter / updateReview methods. Keeps name-validation,
  amount-validation, accounts-required validation, avatar-preview-from-
  name, and the suggested-pace computation — that one now writes into
  an inline `<p data-goal-form-target="suggested">` below the
  target_date field instead of the review card.
- `new.html.erb`: drops the `Step 1 of 2 · Goal details` subtitle
  target. New `goals.new.subtitle` replaces the two step subtitles.
- `edit.html.erb`: renders the same `form` partial.
- `_color_picker.html.erb`: `data-goal-stepper-target="avatarPreview"`
  → `data-goal-form-target="avatarPreview"` (same Stimulus target,
  renamed for the new controller scope).
- `funding_accounts_breakdown_component.rb`: i18n key path moves to
  `goals.form.subtypes.*` matching the locale restructure.
- `en.yml`: `goals.form_stepper.step1.fields.*` → `goals.form.fields.*`.
  `step2.*` and the `back` / `continue` / `cancel` keys drop. New
  `goals.form.create` ("Create goal") + `goals.form.save` ("Save
  changes") drive the submit-button label.

UX delta: the user no longer sees a "Step 1 of 2 / Step 2 of 2" beat.
The form is short enough that everything fits in one panel; the only
value-add from the old step 2 — the suggested-pace hint — now updates
live inline as the amount / date / account-count changes.

All 20 `test/controllers/goals_controller_test.rb` tests still pass.
`bundle exec erb_lint` clean on the touched templates.
2026-05-27 10:34:55 +02:00

90 lines
4.8 KiB
Plaintext

<%# locals: (goal:, linkable_accounts:, currently_linked_account_ids: []) %>
<div data-controller="goal-form"
data-goal-form-currency-value="<%= Current.family.primary_currency_code %>"
data-goal-form-suggested-with-date-value="<%= t("goals.form.suggested_with_date") %>"
data-goal-form-suggested-no-date-value="<%= t("goals.form.suggested_no_date") %>">
<% if goal.errors[:base].any? %>
<%= render "shared/form_errors", model: goal %>
<% end %>
<%= styled_form_with model: goal,
url: goal.persisted? ? goal_path(goal) : goals_path,
method: goal.persisted? ? :patch : :post,
class: "space-y-5" do |f| %>
<div class="flex justify-center">
<%= render "color_picker", form: f, colors: Goal::COLORS, icons: Goal::ICONS %>
</div>
<div>
<%= f.text_field :name,
placeholder: t("goals.form.fields.name_placeholder"),
autofocus: true,
required: true,
label: t("goals.form.fields.name"),
data: { goal_form_target: "nameInput", action: "input->goal-form#nameChanged" } %>
<p class="hidden mt-1.5 text-xs text-destructive" data-goal-form-target="nameError"><%= t("goals.form.errors.name_required") %></p>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<%= f.money_field :target_amount,
label: t("goals.form.fields.target_amount"),
hide_currency: true,
required: true,
amount_data: { goal_form_target: "amountInput", action: "input->goal-form#suggestedChanged" } %>
<p class="hidden mt-1.5 text-xs text-destructive" data-goal-form-target="amountError"><%= t("goals.form.errors.amount_required") %></p>
</div>
<%= f.date_field :target_date,
label: t("goals.form.fields.target_date"),
data: { goal_form_target: "dateInput", action: "input->goal-form#suggestedChanged" } %>
</div>
<p class="text-xs text-secondary italic tabular-nums privacy-sensitive hidden" data-goal-form-target="suggested"></p>
<div>
<div class="mb-2">
<span class="block text-sm font-medium text-primary"><%= t("goals.form.fields.funding_accounts") %></span>
<p class="text-xs text-secondary mt-0.5"><%= t("goals.form.fields.funding_accounts_hint") %></p>
</div>
<div class="bg-container-inset rounded-lg p-1">
<% grouped = linkable_accounts.group_by { |a| a.subtype.to_s.presence || "other" } %>
<% grouped.each_with_index do |(subtype, accts), group_idx| %>
<div class="px-3 py-2 text-[11px] font-medium uppercase tracking-wide text-secondary"><%= t("goals.form.subtypes.#{subtype}", default: subtype.titleize) %></div>
<div class="bg-container rounded-md <%= "mb-1" if group_idx < grouped.size - 1 %>">
<% accts.each_with_index do |account, idx| %>
<label class="flex items-center gap-3 px-3 py-2.5 cursor-pointer hover:bg-surface-hover <%= "border-t border-subdued" if idx > 0 %>">
<%= check_box_tag "goal[account_ids][]",
account.id,
currently_linked_account_ids.include?(account.id.to_s),
id: "goal_account_ids_#{account.id}",
class: "checkbox checkbox--light shrink-0",
data: {
goal_form_target: "linkedAccountCheckbox",
action: "change->goal-form#linkedAccountChanged"
} %>
<%= render Goals::AvatarComponent.new(name: account.name, color: Goals::AvatarComponent.color_for(account.name), size: "md") %>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-primary truncate"><%= account.name %></p>
<p class="text-xs text-secondary"><%= (account.subtype || subtype).titleize %></p>
</div>
<span class="text-sm text-primary tabular-nums privacy-sensitive"><%= Money.new(account.balance, account.currency).format %></span>
</label>
<% end %>
</div>
<% end %>
</div>
<p class="hidden mt-1.5 text-xs text-destructive" data-goal-form-target="accountsError"><%= t("goals.form.errors.accounts_required") %></p>
</div>
<%= f.text_area :notes,
label: t("goals.form.fields.notes"),
rows: 2,
placeholder: t("goals.form.fields.notes_placeholder") %>
<div class="flex justify-end pt-2">
<%= f.submit goal.persisted? ? t("goals.form.save") : t("goals.form.create") %>
</div>
<% end %>
</div>