From 12380dc8ad68cc57f481b89b00c1897bbd7d093b Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 21 Jun 2024 16:23:28 -0400 Subject: [PATCH] Account namespace updates: part 5 (valuations) (#901) * Move Valuation to Account namespace * Move account history to controller * Clean up valuation controller and views * Translations and cleanup * Remove unused scopes and methods * Pass brakeman --- .../account/valuations_controller.rb | 61 +++++++++++++++ app/controllers/accounts_controller.rb | 1 - app/controllers/valuations_controller.rb | 70 ------------------ app/helpers/account/transfers_helper.rb | 2 + app/helpers/account/valuations_helper.rb | 23 ++++++ app/helpers/menus_helper.rb | 17 +++++ app/helpers/transfers_helper.rb | 2 - app/helpers/valuations_helper.rb | 2 - app/models/account.rb | 4 + app/models/account/valuation.rb | 52 +++++++++++++ app/models/time_series/trend.rb | 36 ++++++++- app/models/valuation.rb | 13 ---- app/views/account/valuations/_form.html.erb | 23 ++++++ .../account/valuations/_valuation.html.erb | 50 +++++++++++++ app/views/account/valuations/edit.html.erb | 3 + app/views/account/valuations/index.html.erb | 15 ++++ app/views/account/valuations/new.html.erb | 4 + app/views/account/valuations/show.html.erb | 1 + app/views/accounts/_account_history.html.erb | 29 -------- .../accounts/_account_valuation_list.html.erb | 57 -------------- app/views/accounts/show.html.erb | 28 +++++-- app/views/valuations/_form_row.html.erb | 16 ---- app/views/valuations/create.html.erb | 4 - app/views/valuations/create.turbo_stream.erb | 4 - app/views/valuations/destroy.html.erb | 4 - app/views/valuations/destroy.turbo_stream.erb | 4 - app/views/valuations/edit.html.erb | 8 -- app/views/valuations/new.html.erb | 9 --- app/views/valuations/show.html.erb | 4 - app/views/valuations/update.html.erb | 4 - .../locales/views/account/valuations/en.yml | 26 +++++++ config/locales/views/accounts/en.yml | 10 +-- config/routes.rb | 6 +- .../20240620221801_rename_valuation_table.rb | 5 ++ db/schema.rb | 26 +++---- .../account/valuations_controller_test.rb | 71 ++++++++++++++++++ test/controllers/accounts_controller_test.rb | 4 +- .../transactions_controller_test.rb | 2 +- .../controllers/valuations_controller_test.rb | 74 ------------------- test/fixtures/{ => account}/valuations.yml | 0 test/models/{ => account}/transfer_test.rb | 2 +- test/models/account/valuation_test.rb | 39 ++++++++++ test/models/account_test.rb | 2 +- test/models/time_series/trend_test.rb | 3 + test/models/valuation_test.rb | 4 - 45 files changed, 478 insertions(+), 346 deletions(-) create mode 100644 app/controllers/account/valuations_controller.rb delete mode 100644 app/controllers/valuations_controller.rb create mode 100644 app/helpers/account/transfers_helper.rb create mode 100644 app/helpers/account/valuations_helper.rb delete mode 100644 app/helpers/transfers_helper.rb delete mode 100644 app/helpers/valuations_helper.rb create mode 100644 app/models/account/valuation.rb delete mode 100644 app/models/valuation.rb create mode 100644 app/views/account/valuations/_form.html.erb create mode 100644 app/views/account/valuations/_valuation.html.erb create mode 100644 app/views/account/valuations/edit.html.erb create mode 100644 app/views/account/valuations/index.html.erb create mode 100644 app/views/account/valuations/new.html.erb create mode 100644 app/views/account/valuations/show.html.erb delete mode 100644 app/views/accounts/_account_history.html.erb delete mode 100644 app/views/accounts/_account_valuation_list.html.erb delete mode 100644 app/views/valuations/_form_row.html.erb delete mode 100644 app/views/valuations/create.html.erb delete mode 100644 app/views/valuations/create.turbo_stream.erb delete mode 100644 app/views/valuations/destroy.html.erb delete mode 100644 app/views/valuations/destroy.turbo_stream.erb delete mode 100644 app/views/valuations/edit.html.erb delete mode 100644 app/views/valuations/new.html.erb delete mode 100644 app/views/valuations/show.html.erb delete mode 100644 app/views/valuations/update.html.erb create mode 100644 config/locales/views/account/valuations/en.yml create mode 100644 db/migrate/20240620221801_rename_valuation_table.rb create mode 100644 test/controllers/account/valuations_controller_test.rb delete mode 100644 test/controllers/valuations_controller_test.rb rename test/fixtures/{ => account}/valuations.yml (100%) rename test/models/{ => account}/transfer_test.rb (97%) create mode 100644 test/models/account/valuation_test.rb delete mode 100644 test/models/valuation_test.rb diff --git a/app/controllers/account/valuations_controller.rb b/app/controllers/account/valuations_controller.rb new file mode 100644 index 00000000000..bdaba126451 --- /dev/null +++ b/app/controllers/account/valuations_controller.rb @@ -0,0 +1,61 @@ +class Account::ValuationsController < ApplicationController + before_action :set_account + before_action :set_valuation, only: %i[ show edit update destroy ] + + def new + @valuation = @account.valuations.new + end + + def show + end + + def create + @valuation = @account.valuations.build(valuation_params) + + if @valuation.save + @valuation.sync_account_later + redirect_to account_path(@account), notice: "Valuation created" + else + # TODO: this is not an ideal way to handle errors and should eventually be improved. + # See: https://github.com/hotwired/turbo-rails/pull/367 + flash[:error] = @valuation.errors.full_messages.to_sentence + redirect_to account_path(@account) + end + end + + def edit + end + + def update + if @valuation.update(valuation_params) + @valuation.sync_account_later + redirect_to account_path(@account), notice: t(".success") + else + # TODO: this is not an ideal way to handle errors and should eventually be improved. + # See: https://github.com/hotwired/turbo-rails/pull/367 + flash[:error] = @valuation.errors.full_messages.to_sentence + redirect_to account_path(@account) + end + end + + def destroy + @valuation.destroy! + @valuation.sync_account_later + + redirect_to account_path(@account), notice: t(".success") + end + + private + + def set_account + @account = Current.family.accounts.find(params[:account_id]) + end + + def set_valuation + @valuation = @account.valuations.find(params[:id]) + end + + def valuation_params + params.require(:account_valuation).permit(:date, :value, :currency) + end +end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index a0576b3af04..4aa41e54dda 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -35,7 +35,6 @@ def new def show @balance_series = @account.series(period: @period) - @valuation_series = @account.valuations.to_series end def edit diff --git a/app/controllers/valuations_controller.rb b/app/controllers/valuations_controller.rb deleted file mode 100644 index 1c2979ea69b..00000000000 --- a/app/controllers/valuations_controller.rb +++ /dev/null @@ -1,70 +0,0 @@ -class ValuationsController < ApplicationController - before_action :set_valuation, only: %i[ edit update destroy ] - def create - @account = Current.family.accounts.find(params[:account_id]) - - # TODO: placeholder logic until we have a better abstraction for trends - @valuation = @account.valuations.new(valuation_params.merge(currency: @account.currency)) - if @valuation.save - @valuation.account.sync_later(@valuation.date) - - respond_to do |format| - format.html { redirect_to account_path(@account), notice: "Valuation created" } - format.turbo_stream - end - else - render :new, status: :unprocessable_entity - end - rescue ActiveRecord::RecordNotUnique - flash.now[:error] = "Valuation already exists for this date" - render :new, status: :unprocessable_entity - end - - def show - @valuation = Current.family.accounts.find(params[:account_id]).valuations.find(params[:id]) - end - - def edit - end - - def update - sync_start_date = [ @valuation.date, Date.parse(valuation_params[:date]) ].compact.min - if @valuation.update(valuation_params) - @valuation.account.sync_later(sync_start_date) - - redirect_to account_path(@valuation.account), notice: "Valuation updated" - else - render :edit, status: :unprocessable_entity - end - rescue ActiveRecord::RecordNotUnique - flash.now[:error] = "Valuation already exists for this date" - render :edit, status: :unprocessable_entity - end - - def destroy - @account = @valuation.account - sync_start_date = @account.valuations.where("date < ?", @valuation.date).order(date: :desc).first&.date - @valuation.destroy! - @account.sync_later(sync_start_date) - - respond_to do |format| - format.html { redirect_to account_path(@account), notice: "Valuation deleted" } - format.turbo_stream - end - end - - def new - @account = Current.family.accounts.find(params[:account_id]) - @valuation = @account.valuations.new - end - - private - # Use callbacks to share common setup or constraints between actions. - def set_valuation - @valuation = Valuation.find(params[:id]) - end - - def valuation_params - params.require(:valuation).permit(:date, :value) - end -end diff --git a/app/helpers/account/transfers_helper.rb b/app/helpers/account/transfers_helper.rb new file mode 100644 index 00000000000..ba7a95ae7c0 --- /dev/null +++ b/app/helpers/account/transfers_helper.rb @@ -0,0 +1,2 @@ +module Account::TransfersHelper +end diff --git a/app/helpers/account/valuations_helper.rb b/app/helpers/account/valuations_helper.rb new file mode 100644 index 00000000000..e1a93dde62d --- /dev/null +++ b/app/helpers/account/valuations_helper.rb @@ -0,0 +1,23 @@ +module Account::ValuationsHelper + def valuation_icon(valuation) + if valuation.first_of_series? + "keyboard" + elsif valuation.trend.direction.up? + "arrow-up" + elsif valuation.trend.direction.down? + "arrow-down" + else + "minus" + end + end + + def valuation_style(valuation) + color = valuation.first_of_series? ? "#D444F1" : valuation.trend.color + + <<-STYLE.strip + background-color: color-mix(in srgb, #{color} 5%, white); + border-color: color-mix(in srgb, #{color} 10%, white); + color: #{color}; + STYLE + end +end diff --git a/app/helpers/menus_helper.rb b/app/helpers/menus_helper.rb index 9576d54fa71..41907fa0ee4 100644 --- a/app/helpers/menus_helper.rb +++ b/app/helpers/menus_helper.rb @@ -6,6 +6,23 @@ def contextual_menu(&block) end end + def contextual_menu_modal_action_item(label, url, icon: "pencil-line", turbo_frame: nil) + link_to url, class: "flex items-center rounded-lg text-gray-900 hover:bg-gray-50 py-2 px-3 gap-2", data: { turbo_frame: } do + concat(lucide_icon(icon, class: "shrink-0 w-5 h-5 text-gray-500")) + concat(tag.span(label, class: "text-sm")) + end + end + + def contextual_menu_destructive_item(label, url, turbo_confirm: true, turbo_frame: nil) + button_to url, + method: :delete, + class: "flex items-center w-full rounded-lg text-red-500 hover:bg-red-500/5 py-2 px-3 gap-2", + data: { turbo_confirm: turbo_confirm, turbo_frame: } do + concat(lucide_icon("trash-2", class: "shrink-0 w-5 h-5")) + concat(tag.span(label, class: "text-sm")) + end + end + private def contextual_menu_icon tag.button class: "flex hover:bg-gray-100 p-2 rounded", data: { menu_target: "button" } do diff --git a/app/helpers/transfers_helper.rb b/app/helpers/transfers_helper.rb deleted file mode 100644 index 98355baf594..00000000000 --- a/app/helpers/transfers_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module TransfersHelper -end diff --git a/app/helpers/valuations_helper.rb b/app/helpers/valuations_helper.rb deleted file mode 100644 index 9fe5ad7fc8e..00000000000 --- a/app/helpers/valuations_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ValuationsHelper -end diff --git a/app/models/account.rb b/app/models/account.rb index dee4cae72db..91b7532a5dd 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -29,6 +29,10 @@ def balance_on(date) balances.where("date <= ?", date).order(date: :desc).first&.balance end + def favorable_direction + classification == "asset" ? "up" : "down" + end + # e.g. Wise, Revolut accounts that have transactions in multiple currencies def multi_currency? currencies = [ valuations.pluck(:currency), transactions.pluck(:currency) ].flatten.uniq diff --git a/app/models/account/valuation.rb b/app/models/account/valuation.rb new file mode 100644 index 00000000000..ddbb519cbf1 --- /dev/null +++ b/app/models/account/valuation.rb @@ -0,0 +1,52 @@ +class Account::Valuation < ApplicationRecord + include Monetizable + + monetize :value + + belongs_to :account + + validates :account, :date, :value, presence: true + validates :date, uniqueness: { scope: :account_id } + + scope :chronological, -> { order(:date) } + scope :reverse_chronological, -> { order(date: :desc) } + + def trend + @trend ||= create_trend + end + + def first_of_series? + account.valuations.chronological.limit(1).pluck(:date).first == self.date + end + + def last_of_series? + account.valuations.reverse_chronological.limit(1).pluck(:date).first == self.date + end + + def sync_account_later + if destroyed? + sync_start_date = previous_valuation&.date + else + sync_start_date = [ date_previously_was, date ].compact.min + end + + account.sync_later(sync_start_date) + end + + private + + def previous_valuation + @previous_valuation ||= self.account + .valuations + .where("date < ?", date) + .order(date: :desc) + .first + end + + def create_trend + TimeSeries::Trend.new \ + current: self.value, + previous: previous_valuation&.value, + favorable_direction: account.favorable_direction + end +end diff --git a/app/models/time_series/trend.rb b/app/models/time_series/trend.rb index f62090fbaee..88fdd3a1399 100644 --- a/app/models/time_series/trend.rb +++ b/app/models/time_series/trend.rb @@ -1,16 +1,15 @@ class TimeSeries::Trend include ActiveModel::Validations - attr_reader :current, :previous - - delegate :favorable_direction, to: :series + attr_reader :current, :previous, :favorable_direction validate :values_must_be_of_same_type, :values_must_be_of_known_type - def initialize(current:, previous:, series: nil) + def initialize(current:, previous:, series: nil, favorable_direction: nil) @current = current @previous = previous @series = series + @favorable_direction = get_favorable_direction(favorable_direction) validate! end @@ -25,6 +24,17 @@ def direction end.inquiry end + def color + case direction + when "up" + favorable_direction.down? ? red_hex : green_hex + when "down" + favorable_direction.down? ? green_hex : red_hex + else + gray_hex + end + end + def value if previous.nil? current.is_a?(Money) ? Money.new(0) : 0 @@ -56,8 +66,21 @@ def as_json end private + attr_reader :series + def red_hex + "#F13636" # red-500 + end + + def green_hex + "#10A861" # green-600 + end + + def gray_hex + "#737373" # gray-500 + end + def values_must_be_of_same_type unless current.class == previous.class || [ previous, current ].any?(&:nil?) errors.add :current, "must be of the same type as previous" @@ -90,4 +113,9 @@ def extract_numeric(obj) obj end end + + def get_favorable_direction(favorable_direction) + direction = favorable_direction.presence || series&.favorable_direction + (direction.presence_in(TimeSeries::DIRECTIONS) || "up").inquiry + end end diff --git a/app/models/valuation.rb b/app/models/valuation.rb deleted file mode 100644 index 73a823e6e96..00000000000 --- a/app/models/valuation.rb +++ /dev/null @@ -1,13 +0,0 @@ -class Valuation < ApplicationRecord - include Monetizable - - belongs_to :account - validates :account, :date, :value, presence: true - monetize :value - - scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) } - - def self.to_series - TimeSeries.from_collection all, :value_money - end -end diff --git a/app/views/account/valuations/_form.html.erb b/app/views/account/valuations/_form.html.erb new file mode 100644 index 00000000000..6027bcddad4 --- /dev/null +++ b/app/views/account/valuations/_form.html.erb @@ -0,0 +1,23 @@ +<%# locals: (valuation:) %> +<%= form_with model: valuation, + data: { turbo_frame: "_top" }, + url: valuation.new_record? ? account_valuations_path(valuation.account) : account_valuation_path(valuation.account, valuation), + builder: ActionView::Helpers::FormBuilder do |f| %> +
+
+
+ <%= lucide_icon("pencil-line", class: "w-4 h-4 text-gray-500") %> +
+
+ <%= f.date_field :date, required: "required", max: Date.today, class: "border border-alpha-black-200 bg-white rounded-lg shadow-xs min-w-[200px] px-3 py-1.5 text-gray-900 text-sm" %> + <%= f.number_field :value, required: "required", placeholder: "0.00", step: "0.01", class: "bg-white border border-alpha-black-200 rounded-lg shadow-xs text-gray-900 text-sm px-3 py-1.5 text-right" %> + <%= f.hidden_field :currency, value: valuation.account.currency %> +
+
+ +
+ <%= link_to t(".cancel"), account_valuations_path(valuation.account), class: "text-sm text-gray-900 hover:text-gray-800 font-medium px-3 py-1.5" %> + <%= f.submit class: "bg-gray-50 rounded-lg font-medium px-3 py-1.5 cursor-pointer hover:bg-gray-100 text-sm" %> +
+
+<% end %> diff --git a/app/views/account/valuations/_valuation.html.erb b/app/views/account/valuations/_valuation.html.erb new file mode 100644 index 00000000000..2d496e98944 --- /dev/null +++ b/app/views/account/valuations/_valuation.html.erb @@ -0,0 +1,50 @@ +<%# locals: (valuation:) %> + +<%= turbo_frame_tag dom_id(valuation) do %> +
+
+ <%= tag.div class: "w-8 h-8 rounded-full p-1.5 flex items-center justify-center", style: valuation_style(valuation).html_safe do %> + <%= lucide_icon valuation_icon(valuation), class: "w-4 h-4" %> + <% end %> + +
+ <%= tag.p valuation.date, class: "text-gray-900 font-medium" %> + <%= tag.p valuation.first_of_series? ? t(".start_balance") : t(".value_update"), class: "text-gray-500" %> +
+
+ +
+ <%= tag.p format_money(valuation.value_money), class: "font-medium text-sm text-gray-900" %> +
+ +
+ <% if valuation.trend.direction.flat? %> + <%= tag.span t(".no_change"), class: "text-gray-500" %> + <% else %> + <%= tag.span format_money(valuation.trend.value) %> + <%= tag.span "(#{valuation.trend.percent}%)" %> + <% end %> +
+ +
+ <%= contextual_menu do %> +
+ <%= contextual_menu_modal_action_item t(".edit_entry"), edit_account_valuation_path(valuation.account, valuation) %> + + <%= contextual_menu_destructive_item t(".delete_entry"), + account_valuation_path(valuation.account, valuation), + turbo_frame: "_top", + turbo_confirm: { + title: t(".confirm_title"), + body: t(".confirm_body_html"), + accept: t(".confirm_accept") + } %> +
+ <% end %> +
+
+ + <% unless valuation.last_of_series? %> +
+ <% end %> +<% end %> diff --git a/app/views/account/valuations/edit.html.erb b/app/views/account/valuations/edit.html.erb new file mode 100644 index 00000000000..afa3877d2a9 --- /dev/null +++ b/app/views/account/valuations/edit.html.erb @@ -0,0 +1,3 @@ +<%= turbo_frame_tag dom_id(@valuation) do %> + <%= render "form", valuation: @valuation %> +<% end %> diff --git a/app/views/account/valuations/index.html.erb b/app/views/account/valuations/index.html.erb new file mode 100644 index 00000000000..4fa523bb6e1 --- /dev/null +++ b/app/views/account/valuations/index.html.erb @@ -0,0 +1,15 @@ +<%= turbo_frame_tag dom_id(@account, "valuations") do %> +
+ <%= tag.p t(".date"), class: "col-span-5" %> + <%= tag.p t(".value"), class: "col-span-2 justify-self-end" %> + <%= tag.p t(".change"), class: "col-span-2 justify-self-end" %> + <%= tag.div class: "col-span-1" %> +
+ +
+ <%= turbo_frame_tag dom_id(Account::Valuation.new) %> + <% @account.valuations.reverse_chronological.each do |valuation| %> + <%= render valuation %> + <% end %> +
+<% end %> diff --git a/app/views/account/valuations/new.html.erb b/app/views/account/valuations/new.html.erb new file mode 100644 index 00000000000..29332c174cc --- /dev/null +++ b/app/views/account/valuations/new.html.erb @@ -0,0 +1,4 @@ +<%= turbo_frame_tag dom_id(@valuation) do %> + <%= render "form", valuation: @valuation %> +
+<% end %> diff --git a/app/views/account/valuations/show.html.erb b/app/views/account/valuations/show.html.erb new file mode 100644 index 00000000000..22ab5efeb6c --- /dev/null +++ b/app/views/account/valuations/show.html.erb @@ -0,0 +1 @@ +<%= render "valuation", valuation: @valuation %> diff --git a/app/views/accounts/_account_history.html.erb b/app/views/accounts/_account_history.html.erb deleted file mode 100644 index 19cb16ffea1..00000000000 --- a/app/views/accounts/_account_history.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -<%# locals: (account:, valuations:) %> -
-
-

History

- <%= link_to new_account_valuation_path(account), data: { turbo_frame: dom_id(Valuation.new) }, class: "flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg" do %> - <%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %> - New entry - <% end %> -
-
-
-
-
date
-
-
-
value
-
-
change
-
-
-
- <%= turbo_frame_tag dom_id(Valuation.new) %> - <%= turbo_frame_tag "valuations_list" do %> - <%= render partial: "accounts/account_valuation_list", locals: { valuation_series: valuations } %> - <% end %> -
-
-
-
diff --git a/app/views/accounts/_account_valuation_list.html.erb b/app/views/accounts/_account_valuation_list.html.erb deleted file mode 100644 index 7e56de14a99..00000000000 --- a/app/views/accounts/_account_valuation_list.html.erb +++ /dev/null @@ -1,57 +0,0 @@ -<%# locals: (valuation_series:) %> -<% valuation_series.values.reverse_each.with_index do |valuation, index| %> - <% valuation_styles = trend_styles(valuation.trend) %> - <%= turbo_frame_tag dom_id(valuation.original) do %> -
-
-
- <%= lucide_icon(valuation_styles[:icon], class: "w-4 h-4 #{valuation_styles[:text_class]}") %> -
-
-
-
-

<%= valuation.date %>

- <%# TODO: Add descriptive name of valuation %> -

Manually entered

-
-
<%= format_money valuation.value %>
-
-
- <% if valuation.trend.value == 0 %> - No change - <% else %> - <%= valuation_styles[:symbol] %><%= format_money valuation.trend.value.abs %> - (<%= lucide_icon(valuation_styles[:icon], class: "w-4 h-4 align-text-bottom inline") %> <%= valuation.trend.percent %>%) - <% end %> -
-
- - -
-
- <% unless index == valuation_series.values.size - 1 %> -
- <% end %> - <% end %> -<% end %> diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index 1e5187e3a46..38c76f6fa50 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -56,11 +56,11 @@
<%= render partial: "shared/value_heading", locals: { - label: "Total Value", - period: @period, - value: @account.balance_money, - trend: @balance_series.trend - } %> + label: "Total Value", + period: @period, + value: @account.balance_money, + trend: @balance_series.trend + } %>
<%= form_with url: account_path(@account), method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do %> <%= render partial: "shared/period_select", locals: { value: @period.name } %> @@ -77,7 +77,23 @@
- <%= render partial: "accounts/account_history", locals: { account: @account, valuations: @valuation_series } %> +
+
+ <%= tag.h2 t(".valuations"), class: "font-medium text-lg" %> + <%= link_to new_account_valuation_path(@account), data: { turbo_frame: dom_id(Account::Valuation.new) }, class: "flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg" do %> + <%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %> + <%= tag.span t(".new_entry"), class: "text-sm" %> + <% end %> +
+ +
+ <%= turbo_frame_tag dom_id(@account, "valuations"), src: account_valuations_path(@account) do %> +
+ <%= tag.p t(".loading_history"), class: "text-gray-500 animate-pulse text-sm" %> +
+ <% end %> +
+