diff --git a/.gitignore b/.gitignore index e6b243328..19cc81a3a 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,7 @@ logs/security/ # Added by codex .codex + +# Ignore Playwright artifacts +playwright-report/ +.playwright-mcp diff --git a/app/components/savings/goal_card_component.html.erb b/app/components/savings/goal_card_component.html.erb index 23b292140..550c7844c 100644 --- a/app/components/savings/goal_card_component.html.erb +++ b/app/components/savings/goal_card_component.html.erb @@ -15,8 +15,8 @@ <%= goal.current_balance_money.format %> / <%= goal.target_amount_money.format %> -
-
+
+
<%= progress_percent %>% diff --git a/app/components/savings/goal_card_component.rb b/app/components/savings/goal_card_component.rb index 738f5b6a3..554bf862c 100644 --- a/app/components/savings/goal_card_component.rb +++ b/app/components/savings/goal_card_component.rb @@ -21,10 +21,11 @@ class Savings::GoalCardComponent < ApplicationComponent end def bar_color_class - case progress_percent - when 0...25 then "bg-gray-400" - when 25...75 then "bg-blue-500" - else "bg-green-600" + case goal.status + when :reached then "bg-green-500" + when :behind then "bg-yellow-500" + when :on_track then "bg-blue-500" + else "bg-gray-400" end end end diff --git a/app/components/savings/progress_ring_component.html.erb b/app/components/savings/progress_ring_component.html.erb index 871fab614..55b313104 100644 --- a/app/components/savings/progress_ring_component.html.erb +++ b/app/components/savings/progress_ring_component.html.erb @@ -20,8 +20,8 @@ transform="rotate(-90 <%= Savings::ProgressRingComponent::SIZE / 2.0 %> <%= Savings::ProgressRingComponent::SIZE / 2.0 %>)" />
- <%= percent %>% - <%= current_label %> - of <%= target_label %> + <%= t("savings_goals.show.ring.saved") %> + <%= current_label %> + of <%= target_label %>
diff --git a/app/components/savings/progress_ring_component.rb b/app/components/savings/progress_ring_component.rb index b67f4b9ae..4989f2b1b 100644 --- a/app/components/savings/progress_ring_component.rb +++ b/app/components/savings/progress_ring_component.rb @@ -1,5 +1,5 @@ class Savings::ProgressRingComponent < ApplicationComponent - SIZE = 180 + SIZE = 220 STROKE = 14 RADIUS = (SIZE - STROKE) / 2.0 CIRCUMFERENCE = 2 * Math::PI * RADIUS @@ -19,10 +19,11 @@ class Savings::ProgressRingComponent < ApplicationComponent end def stroke_color - case percent - when 0...25 then "var(--color-gray-400)" - when 25...75 then "var(--color-blue-500)" - else "var(--color-green-600)" + case goal.status + when :reached then "var(--color-green-500)" + when :behind then "var(--color-yellow-500)" + when :on_track then "var(--color-blue-500)" + else "var(--color-gray-400)" end end diff --git a/app/components/savings/status_pill_component.html.erb b/app/components/savings/status_pill_component.html.erb index 8db06d50a..b1037bb75 100644 --- a/app/components/savings/status_pill_component.html.erb +++ b/app/components/savings/status_pill_component.html.erb @@ -1,4 +1,4 @@ - - <%= helpers.icon(icon_name, size: "xs", color: "current") %> + + <%= helpers.icon(icon_name, size: "sm", color: icon_color) %> <%= label %> diff --git a/app/components/savings/status_pill_component.rb b/app/components/savings/status_pill_component.rb index fa26f5575..bac6d9fdb 100644 --- a/app/components/savings/status_pill_component.rb +++ b/app/components/savings/status_pill_component.rb @@ -1,9 +1,9 @@ class Savings::StatusPillComponent < ApplicationComponent VARIANTS = { - on_track: { classes: "bg-green-600/10 text-green-700", icon: "check" }, - behind: { classes: "bg-yellow-500/10 text-yellow-700", icon: "alert-triangle" }, - reached: { classes: "bg-green-600/10 text-green-700", icon: "circle-check-big" }, - no_target_date: { classes: "bg-container-inset text-secondary", icon: "calendar-off" } + on_track: { classes: "bg-green-500/10 text-success", icon: "check-circle", icon_color: "green" }, + behind: { classes: "bg-yellow-500/10 text-warning", icon: "alert-triangle", icon_color: "yellow" }, + reached: { classes: "bg-green-500/10 text-success", icon: "circle-check-big", icon_color: "green" }, + no_target_date: { classes: "bg-surface-inset text-secondary", icon: "calendar-off", icon_color: "default" } }.freeze def initialize(goal:) @@ -26,6 +26,10 @@ class Savings::StatusPillComponent < ApplicationComponent variant[:icon] end + def icon_color + variant[:icon_color] + end + def classes variant[:classes] end diff --git a/app/javascript/controllers/savings_goal_stepper_controller.js b/app/javascript/controllers/savings_goal_stepper_controller.js index 913d5be69..64dd380b4 100644 --- a/app/javascript/controllers/savings_goal_stepper_controller.js +++ b/app/javascript/controllers/savings_goal_stepper_controller.js @@ -52,10 +52,13 @@ export default class extends Controller { } validateStep1() { + const requiredInputs = this.step1PanelTarget.querySelectorAll( + 'input[name="savings_goal[name]"], input[name="savings_goal[target_amount]"]' + ); let ok = true; - this.step1FieldTargets.forEach((field) => { - if (!field.checkValidity()) { - field.reportValidity(); + requiredInputs.forEach((input) => { + if (typeof input.checkValidity === "function" && !input.checkValidity()) { + input.reportValidity(); ok = false; } }); @@ -95,11 +98,13 @@ export default class extends Controller { updateReview() { if (!this.hasReviewPanelTarget) return; - if (this.hasReviewNameTarget && this.hasNameFieldTarget) { - this.reviewNameTarget.textContent = this.nameFieldTarget.value || "—"; + if (this.hasReviewNameTarget) { + const nameInput = this.element.querySelector('input[name="savings_goal[name]"]'); + this.reviewNameTarget.textContent = nameInput?.value || "—"; } - if (this.hasReviewAmountTarget && this.hasTargetAmountFieldTarget) { - this.reviewAmountTarget.textContent = this.targetAmountFieldTarget.value || "—"; + if (this.hasReviewAmountTarget) { + const amountInput = this.element.querySelector('input[name="savings_goal[target_amount]"]'); + this.reviewAmountTarget.textContent = amountInput?.value || "—"; } if (this.hasReviewDateTarget) { const dateInput = this.element.querySelector('input[type="date"][name="savings_goal[target_date]"]'); diff --git a/app/models/demo/generator.rb b/app/models/demo/generator.rb index 0b35b9a40..1e226f86b 100644 --- a/app/models/demo/generator.rb +++ b/app/models/demo/generator.rb @@ -1279,7 +1279,7 @@ class Demo::Generator end def generate_savings_goals!(family) - depository_accounts = family.accounts.depository.visible.to_a + depository_accounts = family.accounts.where(accountable_type: "Depository").visible.to_a return if depository_accounts.empty? currency = depository_accounts.first.currency diff --git a/app/views/savings_goals/index.html.erb b/app/views/savings_goals/index.html.erb index 40489c9c0..5d2da4dd4 100644 --- a/app/views/savings_goals/index.html.erb +++ b/app/views/savings_goals/index.html.erb @@ -1,20 +1,20 @@ -<%= content_for :page_title, t(".title") %> -<%= content_for :page_actions do %> - <% if @linkable_account_count > 0 %> - <%= render DS::Link.new( - text: t(".new_goal"), - variant: "primary", - href: new_savings_goal_path, - icon: "plus", - frame: :modal - ) %> - <% end %> -<% end %> +
+
+

<%= t(".title") %>

+ <% if @linkable_account_count > 0 %> + <%= render DS::Link.new( + text: t(".new_goal"), + variant: "primary", + href: new_savings_goal_path, + icon: "plus", + frame: :modal + ) %> + <% end %> +
-<% if @savings_goals.empty? && @counts["all"].zero? %> - <%= render "empty_state", linkable_account_count: @linkable_account_count %> -<% else %> -
+ <% if @savings_goals.empty? && @counts["all"].zero? %> + <%= render "empty_state", linkable_account_count: @linkable_account_count %> + <% else %>
<% end %> -
-<% end %> + <% end %> +
diff --git a/app/views/savings_goals/show.html.erb b/app/views/savings_goals/show.html.erb index 50b5e6298..f32884ca8 100644 --- a/app/views/savings_goals/show.html.erb +++ b/app/views/savings_goals/show.html.erb @@ -1,73 +1,80 @@ -<%= content_for :page_title, @savings_goal.name %> -<%= content_for :page_actions do %> - <%= render DS::Menu.new do |menu| %> - <% menu.with_item( - variant: "link", - text: t(".edit"), - icon: "pencil", - href: edit_savings_goal_path(@savings_goal), - data: { turbo_frame: :modal } - ) %> - <% if @savings_goal.may_pause? %> - <% menu.with_item( - variant: "button", - text: t(".pause"), - icon: "pause", - href: pause_savings_goal_path(@savings_goal), - method: :patch - ) %> - <% end %> - <% if @savings_goal.may_resume? %> - <% menu.with_item( - variant: "button", - text: t(".resume"), - icon: "play", - href: resume_savings_goal_path(@savings_goal), - method: :patch - ) %> - <% end %> - <% if @savings_goal.may_complete? %> - <% menu.with_item( - variant: "button", - text: t(".complete"), - icon: "circle-check-big", - href: complete_savings_goal_path(@savings_goal), - method: :patch - ) %> - <% end %> - <% if @savings_goal.may_archive? %> - <% menu.with_item( - variant: "button", - text: t(".archive"), - icon: "archive", - href: archive_savings_goal_path(@savings_goal), - method: :patch - ) %> - <% end %> - <% if @savings_goal.may_unarchive? %> - <% menu.with_item( - variant: "button", - text: t(".unarchive"), - icon: "archive-restore", - href: unarchive_savings_goal_path(@savings_goal), - method: :patch - ) %> - <% end %> - <% if @savings_goal.archived? %> - <% menu.with_item( - variant: "button", - text: t(".delete"), - icon: "trash-2", - href: savings_goal_path(@savings_goal), - method: :delete, - destructive: true, - confirm: CustomConfirm.for_resource_deletion(@savings_goal.name, high_severity: true) - ) %> - <% end %> - <% end %> -<% end %> -
+
+
+ <%= render Savings::GoalAvatarComponent.new(goal: @savings_goal, size: "md") %> +
+

<%= @savings_goal.name %>

+

<%= t("savings_goals.states.#{@savings_goal.state}") %>

+
+
+ + <%= render DS::Menu.new do |menu| %> + <% menu.with_item( + variant: "link", + text: t(".edit"), + icon: "pencil", + href: edit_savings_goal_path(@savings_goal), + data: { turbo_frame: :modal } + ) %> + <% if @savings_goal.may_pause? %> + <% menu.with_item( + variant: "button", + text: t(".pause"), + icon: "pause", + href: pause_savings_goal_path(@savings_goal), + method: :patch + ) %> + <% end %> + <% if @savings_goal.may_resume? %> + <% menu.with_item( + variant: "button", + text: t(".resume"), + icon: "play", + href: resume_savings_goal_path(@savings_goal), + method: :patch + ) %> + <% end %> + <% if @savings_goal.may_complete? %> + <% menu.with_item( + variant: "button", + text: t(".complete"), + icon: "circle-check-big", + href: complete_savings_goal_path(@savings_goal), + method: :patch + ) %> + <% end %> + <% if @savings_goal.may_archive? %> + <% menu.with_item( + variant: "button", + text: t(".archive"), + icon: "archive", + href: archive_savings_goal_path(@savings_goal), + method: :patch + ) %> + <% end %> + <% if @savings_goal.may_unarchive? %> + <% menu.with_item( + variant: "button", + text: t(".unarchive"), + icon: "archive-restore", + href: unarchive_savings_goal_path(@savings_goal), + method: :patch + ) %> + <% end %> + <% if @savings_goal.archived? %> + <% menu.with_item( + variant: "button", + text: t(".delete"), + icon: "trash-2", + href: savings_goal_path(@savings_goal), + method: :delete, + destructive: true, + confirm: CustomConfirm.for_resource_deletion(@savings_goal.name, high_severity: true) + ) %> + <% end %> + <% end %> +
+
@@ -75,14 +82,6 @@
-
- <%= render Savings::GoalAvatarComponent.new(goal: @savings_goal, size: "lg") %> -
-

<%= @savings_goal.name %>

-

<%= t("savings_goals.states.#{@savings_goal.state}") %>

-
-
-

<%= t(".stats.current") %>

diff --git a/config/locales/views/savings_goals/en.yml b/config/locales/views/savings_goals/en.yml index db67484b4..152e9c893 100644 --- a/config/locales/views/savings_goals/en.yml +++ b/config/locales/views/savings_goals/en.yml @@ -51,6 +51,8 @@ en: funding_accounts_heading: Funding accounts no_contributions_yet: No contributions yet. confirm_delete_contribution: Delete this contribution? + ring: + saved: Saved source: initial: Initial manual: Manual