mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-08 23:15:24 +02:00
fix(simplefin): Fix some querying issues and various rendering bugs
- Also adding some nice looking progress indicators for loading SimpleFIN data
This commit is contained in:
parent
8d22d46420
commit
72c6840f5a
17 changed files with 209 additions and 156 deletions
|
@ -3,16 +3,11 @@
|
||||||
class SimpleFinController < ApplicationController
|
class SimpleFinController < ApplicationController
|
||||||
before_action :set_accountable_type
|
before_action :set_accountable_type
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_simple_fin_provider, only: %i[create new]
|
before_action :set_simple_fin_provider, only: %i[create new accounts_list]
|
||||||
before_action :require_simple_fin_provider, only: %i[create new]
|
before_action :require_simple_fin_provider, only: %i[create new accounts_list]
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@simple_fin_accounts = @simple_fin_provider.get_available_accounts(@accountable_type)
|
# This action now simply renders the view, which will trigger the Turbo Frame load.
|
||||||
# Filter accounts we already have
|
|
||||||
@simple_fin_accounts = @simple_fin_accounts.filter { |acc| !account_exists(acc) }
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error "SimpleFIN: Failed to fetch accounts - #{e.message}"
|
|
||||||
redirect_to root_path, alert: t(".fetch_failed")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -21,6 +16,23 @@ class SimpleFinController < ApplicationController
|
||||||
Current.family.accounts.find_by(name: acc["name"])
|
Current.family.accounts.find_by(name: acc["name"])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def accounts_list
|
||||||
|
simple_fin_accounts_data = @simple_fin_provider.get_available_accounts(@accountable_type)
|
||||||
|
# Filter accounts we already have
|
||||||
|
@simple_fin_accounts = simple_fin_accounts_data.filter { |acc| !account_exists(acc) }
|
||||||
|
# Implicitly renders app/views/simple_fin/accounts_list.html.erb
|
||||||
|
rescue Provider::SimpleFin::RateLimitExceededError => e
|
||||||
|
@error_message = t("simple_fin.new.rate_limit_hit")
|
||||||
|
# Implicitly renders app/views/simple_fin/accounts_list.html.erb
|
||||||
|
render :accounts_list # Or just let it render implicitly
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error "SimpleFIN: Failed to fetch accounts from accounts_list - #{e.message}"
|
||||||
|
@error_message = t("simple_fin.new.fetch_failed")
|
||||||
|
# Implicitly renders app/views/simple_fin/accounts_list.html.erb
|
||||||
|
render :accounts_list # Or just let it render implicitly
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Requests all accounts to be re-synced
|
# Requests all accounts to be re-synced
|
||||||
def sync
|
def sync
|
||||||
|
@ -80,6 +92,8 @@ class SimpleFinController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to root_path, notice: t(".accounts_created_success")
|
redirect_to root_path, notice: t(".accounts_created_success")
|
||||||
|
rescue Provider::SimpleFin::RateLimitExceededError => e
|
||||||
|
redirect_to new_account_path, alert: t(".rate_limit_hit")
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error "SimpleFIN: Failed to create accounts - #{e.message}"
|
Rails.logger.error "SimpleFIN: Failed to create accounts - #{e.message}"
|
||||||
redirect_to new_simple_fin_path
|
redirect_to new_simple_fin_path
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Assistant::Function::GetAccounts < Assistant::Function
|
||||||
type: account.accountable_type,
|
type: account.accountable_type,
|
||||||
start_date: account.start_date,
|
start_date: account.start_date,
|
||||||
is_plaid_linked: account.plaid_account_id.present?,
|
is_plaid_linked: account.plaid_account_id.present?,
|
||||||
is_simple_fin_linked: account.simple_fin_id.present?,
|
is_simple_fin_linked: account.simple_fin_account_id.present?,
|
||||||
is_active: account.is_active,
|
is_active: account.is_active,
|
||||||
historical_balances: historical_balances(account)
|
historical_balances: historical_balances(account)
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,20 +107,20 @@ class Provider::SimpleFin
|
||||||
endpoint += "&end-date=#{trans_end_date}"
|
endpoint += "&end-date=#{trans_end_date}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# account_info = send_request_to_sf(endpoint)
|
# request_content = send_request_to_sf(endpoint)
|
||||||
# accounts = account_info["accounts"]
|
# # TODO: Remove JSON Reading for real requests. Disabled currently due to preventing rate limits.
|
||||||
# TODO: Remove JSON Reading for real requests. Disabled currently due to preventing rate limits.
|
|
||||||
json_file_path = Rails.root.join("sample.simple.fin.json")
|
json_file_path = Rails.root.join("sample.simple.fin.json")
|
||||||
accounts = []
|
accounts = []
|
||||||
error_messages = []
|
error_messages = []
|
||||||
if File.exist?(json_file_path)
|
if File.exist?(json_file_path)
|
||||||
file_content = File.read(json_file_path)
|
request_content = File.read(json_file_path)
|
||||||
parsed_json = JSON.parse(file_content)
|
|
||||||
accounts = parsed_json["accounts"] || []
|
|
||||||
error_messages = parsed_json["errors"] || []
|
|
||||||
else
|
else
|
||||||
Rails.logger.warn "SimpleFIN: Sample JSON file not found at #{json_file_path}. Returning empty accounts."
|
Rails.logger.warn "SimpleFIN: Sample JSON file not found at #{json_file_path}. Returning empty accounts."
|
||||||
end
|
end
|
||||||
|
# Parse our content
|
||||||
|
parsed_json = JSON.parse(request_content)
|
||||||
|
accounts = parsed_json["accounts"] || []
|
||||||
|
error_messages = parsed_json["errors"] || []
|
||||||
|
|
||||||
|
|
||||||
# The only way we can really determine types right now is by some properties. Try and set their types
|
# The only way we can really determine types right now is by some properties. Try and set their types
|
||||||
|
|
|
@ -35,40 +35,53 @@ class SimpleFinAccount < ApplicationRecord
|
||||||
sfc.simple_fin_accounts.find_or_create_by!(external_id: sf_account_data["id"]) do |sfa|
|
sfc.simple_fin_accounts.find_or_create_by!(external_id: sf_account_data["id"]) do |sfa|
|
||||||
balance = get_adjusted_balance(sf_account_data)
|
balance = get_adjusted_balance(sf_account_data)
|
||||||
sfa.current_balance = balance
|
sfa.current_balance = balance
|
||||||
sfa.available_balance = balance
|
sfa.available_balance = sf_account_data["available-balance"]&.to_d
|
||||||
sfa.currency = sf_account_data["currency"]
|
sfa.currency = sf_account_data["currency"]
|
||||||
|
|
||||||
new_account = sfc.family.accounts.new(
|
|
||||||
name: sf_account_data["name"],
|
|
||||||
balance: 0,
|
|
||||||
currency: sf_account_data["currency"],
|
|
||||||
accountable: TYPE_MAPPING[sf_account_data["type"]].new,
|
|
||||||
subtype: sf_account_data["subtype"],
|
|
||||||
simple_fin_account: sfa, # Explicitly associate back
|
|
||||||
last_synced_at: Time.current, # Mark as synced upon creation
|
|
||||||
# Set cash_balance similar to how Account.create_and_sync might
|
|
||||||
cash_balance: 0
|
|
||||||
)
|
|
||||||
|
|
||||||
new_account.entries.build(
|
if sfa.account
|
||||||
name: "Current Balance",
|
account = sfa.account
|
||||||
date: Date.current,
|
else
|
||||||
amount: balance,
|
sfa.account = sfc.family.accounts.new(
|
||||||
currency: new_account.currency,
|
name: sf_account_data["name"],
|
||||||
entryable: Valuation.new
|
balance: sfa.current_balance,
|
||||||
)
|
currency: sf_account_data["currency"],
|
||||||
new_account.entries.build(
|
accountable: TYPE_MAPPING[sf_account_data["type"]].new,
|
||||||
name: "Initial Balance", # This will be the balance as of "yesterday"
|
subtype: sf_account_data["subtype"],
|
||||||
date: 1.day.ago.to_date,
|
simple_fin_account: sfa, # Explicitly associate back
|
||||||
amount: 0,
|
last_synced_at: Time.current, # Mark as synced upon creation
|
||||||
currency: new_account.currency,
|
# Set cash_balance similar to how Account.create_and_sync might
|
||||||
entryable: Valuation.new
|
cash_balance: sfa.available_balance
|
||||||
)
|
)
|
||||||
|
account = sfa.account
|
||||||
|
account.save!
|
||||||
|
|
||||||
|
transaction do
|
||||||
|
# Create 2 valuations for new accounts to establish a value history for users to see
|
||||||
|
account.entries.build(
|
||||||
|
name: "Current Balance",
|
||||||
|
date: Date.current,
|
||||||
|
amount: sfa.current_balance,
|
||||||
|
currency: account.currency,
|
||||||
|
entryable: Valuation.new
|
||||||
|
)
|
||||||
|
account.entries.build(
|
||||||
|
name: "Initial Balance",
|
||||||
|
date: 1.day.ago.to_date,
|
||||||
|
amount: 0,
|
||||||
|
currency: account.currency,
|
||||||
|
entryable: Valuation.new
|
||||||
|
)
|
||||||
|
|
||||||
|
account.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Make sure SFA is up to date
|
||||||
sfa.save!
|
sfa.save!
|
||||||
new_account.save!
|
sfa.sync_account_data!(sf_account_data)
|
||||||
new_account.sync_later
|
# Sync this account to trick it into showing a correct current balance
|
||||||
sfa.account = new_account
|
account.sync_later
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -81,18 +94,15 @@ class SimpleFinAccount < ApplicationRecord
|
||||||
##
|
##
|
||||||
# Syncs all account data for the given sf_account_data parameter
|
# Syncs all account data for the given sf_account_data parameter
|
||||||
def sync_account_data!(sf_account_data)
|
def sync_account_data!(sf_account_data)
|
||||||
accountable_attributes = { id: self.account.accountable_id }
|
|
||||||
balance = SimpleFinAccount.get_adjusted_balance(sf_account_data)
|
balance = SimpleFinAccount.get_adjusted_balance(sf_account_data)
|
||||||
|
puts "SFA #{sf_account_data} #{self.account.inspect}"
|
||||||
self.update!(
|
self.update!(
|
||||||
current_balance: balance,
|
current_balance: balance,
|
||||||
available_balance: sf_account_data["available-balance"]&.to_d,
|
available_balance: sf_account_data["available-balance"]&.to_d
|
||||||
currency: sf_account_data["currency"],
|
)
|
||||||
account_attributes: {
|
|
||||||
id: self.account.id,
|
self.account.update!(
|
||||||
balance: balance,
|
balance: balance
|
||||||
last_synced_at: Time.current,
|
|
||||||
accountable_attributes: accountable_attributes
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
institution_errors = sf_account_data["org"]["institution_errors"]
|
institution_errors = sf_account_data["org"]["institution_errors"]
|
||||||
|
@ -107,7 +117,7 @@ class SimpleFinAccount < ApplicationRecord
|
||||||
sync_transactions!(sf_account_data["transactions"])
|
sync_transactions!(sf_account_data["transactions"])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sync holdings if present in the data and it's an investment account
|
# Sync holdings if present in the data and it's an investment account. SimpleFIN doesn't support transactions for holdings accounts
|
||||||
if self.account&.investment? && sf_account_data["holdings"].is_a?(Array)
|
if self.account&.investment? && sf_account_data["holdings"].is_a?(Array)
|
||||||
sync_holdings!(sf_account_data["holdings"])
|
sync_holdings!(sf_account_data["holdings"])
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<%# locals: (account:, link_to_path: nil, given_title: nil, target: nil) %>
|
<%# locals: (account:, link_to_path: nil, given_title: nil, target: nil) %>
|
||||||
|
|
||||||
<%# Flag indicators of account issues %>
|
<%# Flag indicators of account issues %>
|
||||||
<% if account.simple_fin_account.simple_fin_item.status == "requires_update" %>
|
<% if account.simple_fin_account&.simple_fin_item&.status == "requires_update" %>
|
||||||
<%= link_to link_to_path, target: target do %>
|
<%= link_to link_to_path, target: target do %>
|
||||||
<%= icon(
|
<%= icon(
|
||||||
"alert-triangle",
|
"alert-triangle",
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
<%= render layout: "accounts/new/container", locals: { title: t(".title") } do %>
|
<%= render layout: "accounts/new/container", locals: { title: t(".title") } do %>
|
||||||
|
|
||||||
|
<%# Display flash messages, especially alerts for errors %>
|
||||||
|
<% if flash[:alert] %>
|
||||||
|
<div class="mb-4 p-4 text-sm text-red-700 bg-red-100 rounded-lg border border-red-300" role="alert">
|
||||||
|
<%= flash[:alert] %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<% unless params[:classification] == "liability" %>
|
<% unless params[:classification] == "liability" %>
|
||||||
<%= render "account_type", accountable: Depository.new %>
|
<%= render "account_type", accountable: Depository.new %>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="bg-container p-5 shadow-border-xs rounded-xl" data-controller="focus-record" data-focus-record-id-value="<%= @focused_record ? dom_id(@focused_record) : nil %>">
|
<div class="bg-container p-5 shadow-border-xs rounded-xl" data-controller="focus-record" data-focus-record-id-value="<%= @focused_record ? dom_id(@focused_record) : nil %>">
|
||||||
<div class="flex items-center justify-between mb-4" data-testid="activity-menu">
|
<div class="flex items-center justify-between mb-4" data-testid="activity-menu">
|
||||||
<%= tag.h2 t(".title"), class: "font-medium text-lg" %>
|
<%= tag.h2 t(".title"), class: "font-medium text-lg" %>
|
||||||
<% unless (@account.plaid_account_id.present? || @account.simple_fin_account.present?) %>
|
<% unless (@account.plaid_account_id.present? || @account.simple_fin_account_id.present?) %>
|
||||||
<%= render MenuComponent.new(variant: "button") do |menu| %>
|
<%= render MenuComponent.new(variant: "button") do |menu| %>
|
||||||
<% menu.with_button(text: "New", variant: "secondary", icon: "plus") %>
|
<% menu.with_button(text: "New", variant: "secondary", icon: "plus") %>
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,8 @@
|
||||||
size: "sm",
|
size: "sm",
|
||||||
href: sync_plaid_item_path(account.plaid_account.plaid_item),
|
href: sync_plaid_item_path(account.plaid_account.plaid_item),
|
||||||
disabled: account.syncing?,
|
disabled: account.syncing?,
|
||||||
frame: :_top
|
frame: :_top,
|
||||||
|
title: "Refresh all SimpleFIN accounts"
|
||||||
) %>
|
) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% elsif account.simple_fin_account_id.present? %>
|
<% elsif account.simple_fin_account_id.present? %>
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
<%= turbo_frame_tag dom_id(@account, "holdings") do %>
|
<%= turbo_frame_tag dom_id(@account, "holdings") do %>
|
||||||
<div class="bg-container space-y-4 p-5 rounded-xl shadow-border-xs">
|
<div class="bg-container space-y-4 p-5 rounded-xl shadow-border-xs">
|
||||||
<div class="flex items-center justify-between">
|
<% unless (@account.plaid_account_id.present? || @account.simple_fin_account_id.present?) %>
|
||||||
<%= tag.h2 t(".holdings"), class: "font-medium text-lg" %>
|
<div class="flex items-center justify-between">
|
||||||
<%= link_to new_trade_path(account_id: @account.id),
|
<%= tag.h2 t(".holdings"), class: "font-medium text-lg" %>
|
||||||
id: dom_id(@account, "new_trade"),
|
<%= link_to new_trade_path(account_id: @account.id),
|
||||||
data: { turbo_frame: :modal },
|
id: dom_id(@account, "new_trade"),
|
||||||
class: "flex gap-1 font-medium items-center bg-gray-50 text-primary p-2 rounded-lg" do %>
|
data: { turbo_frame: :modal },
|
||||||
<span class="text-primary">
|
class: "flex gap-1 font-medium items-center bg-gray-50 text-primary p-2 rounded-lg" do %>
|
||||||
<%= icon("plus", color: "current") %>
|
<span class="text-primary">
|
||||||
</span>
|
<%= icon("plus", color: "current") %>
|
||||||
<%= tag.span t(".new_holding"), class: "text-sm" %>
|
</span>
|
||||||
<% end %>
|
<%= tag.span t(".new_holding"), class: "text-sm" %>
|
||||||
</div>
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="rounded-xl bg-container-inset p-1">
|
<div class="rounded-xl bg-container-inset p-1">
|
||||||
<div class="grid grid-cols-12 items-center uppercase text-xs font-medium text-secondary px-4 py-2">
|
<div class="grid grid-cols-12 items-center uppercase text-xs font-medium text-secondary px-4 py-2">
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% unless @holding.account.plaid_account_id.present? %>
|
<% unless (@holding.account.plaid_account_id.present? || @holding.account.simple_fin_account_id.present? ) %>
|
||||||
<% dialog.with_section(title: t(".settings"), open: true) do %>
|
<% dialog.with_section(title: t(".settings"), open: true) do %>
|
||||||
<div class="pb-4">
|
<div class="pb-4">
|
||||||
<div class="flex items-center justify-between gap-2 p-3">
|
<div class="flex items-center justify-between gap-2 p-3">
|
||||||
|
|
65
app/views/simple_fin/_accounts_list_content.html.erb
Normal file
65
app/views/simple_fin/_accounts_list_content.html.erb
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<%# locals: simple_fin_accounts, accountable_type %>
|
||||||
|
|
||||||
|
<% if simple_fin_accounts.blank? %>
|
||||||
|
<div class="text-center text-gray-500 py-8">
|
||||||
|
<p>No accounts found matching this type from SimpleFIN.</p>
|
||||||
|
<p>Please ensure your SimpleFIN subscription is active.</p>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<%= styled_form_with url: simple_fin_index_path(accountable_type: accountable_type), method: :post, data: { turbo: false } do |form| %>
|
||||||
|
<%# Render each account option parsed from SimpleFIN %>
|
||||||
|
<% simple_fin_accounts.each_with_index do |account, index| %>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="flex items-start justify-between p-4 hover:bg-surface rounded-lg border border-gray-200 cursor-pointer">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<%= check_box_tag 'selected_account_ids[]', account["id"], class: "border-gray-300 rounded" -%>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<%# Account Name %>
|
||||||
|
<div class="font-medium text-primary"><%= account["name"] %></div>
|
||||||
|
<%# Account Source %>
|
||||||
|
<% if account.dig("org", "name").present? %>
|
||||||
|
<div class="text-xs text-gray-400 mt-0.5">
|
||||||
|
Source:
|
||||||
|
<% if account.dig("org", "url").present? %>
|
||||||
|
<%= link_to account.dig("org", "name"), account.dig("org", "url"), target: "_blank", rel: "noopener noreferrer", class: "hover:underline" %>
|
||||||
|
<% else %>
|
||||||
|
<%= account.dig("org", "name") %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%# SimpleFIN has a tough time determining account types. Let the user fill them out here. %>
|
||||||
|
<div class="mt-2">
|
||||||
|
<% if accountable_type == "Depository" %>
|
||||||
|
<%= form.select "account[#{account['id']}][subtype]",
|
||||||
|
Depository::SUBTYPES.map { |k, v| [v[:long], k] },
|
||||||
|
{ label: "Subtype", prompt: t("depositories.form.subtype_prompt"), include_blank: t("depositories.form.none") } %>
|
||||||
|
<% elsif accountable_type == "Investment" %>
|
||||||
|
<%= form.select "account[#{account['id']}][subtype]",
|
||||||
|
Investment::SUBTYPES.map { |k, v| [v[:long], k] },
|
||||||
|
{ label: "Subtype", prompt: t("investments.form.subtype_prompt"), include_blank: t("investments.form.none") } %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# Current balance %>
|
||||||
|
<% currency_unit = Money::Currency.all_instances.find { |currency| currency.iso_code == account["currency"] }.symbol %>
|
||||||
|
<% balance_color_class = account["balance"].to_f < 0 ? "text-red-500" : "text-green-500" %>
|
||||||
|
<div class="text-sm text-gray-700 font-medium">
|
||||||
|
<span class="<%= balance_color_class %>">
|
||||||
|
<%= number_to_currency(account["balance"], unit: currency_unit || "$") %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||||
|
<%= form.submit t("simple_fin.form.add_accounts"), class: "w-full btn btn-primary",
|
||||||
|
data: {
|
||||||
|
disable_with: "Adding Accounts..."
|
||||||
|
} %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
4
app/views/simple_fin/_error_display.html.erb
Normal file
4
app/views/simple_fin/_error_display.html.erb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<%# locals: message, accountable_type %>
|
||||||
|
<div class="text-center text-red-500 py-8">
|
||||||
|
<p><%= message %></p>
|
||||||
|
</div>
|
7
app/views/simple_fin/accounts_list.html.erb
Normal file
7
app/views/simple_fin/accounts_list.html.erb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<turbo-frame id="simple_fin_accounts_data">
|
||||||
|
<% if @error_message %>
|
||||||
|
<%= render partial: "simple_fin/error_display", locals: { message: @error_message, accountable_type: @accountable_type } %>
|
||||||
|
<% else %>
|
||||||
|
<%= render partial: "simple_fin/accounts_list_content", locals: { simple_fin_accounts: @simple_fin_accounts, accountable_type: @accountable_type } %>
|
||||||
|
<% end %>
|
||||||
|
</turbo-frame>
|
|
@ -1,70 +1,14 @@
|
||||||
<%# locals: @simple_fin_accounts %>
|
<%# locals: @simple_fin_accounts %>
|
||||||
|
|
||||||
<%= render layout: "accounts/new/container", locals: { title: "Select SimpleFIN Accounts", back_path: new_account_path } do %>
|
<%= render layout: "accounts/new/container", locals: { title: "Select SimpleFIN Accounts", back_path: new_account_path } do %>
|
||||||
<% if @simple_fin_accounts.blank? %>
|
<turbo-frame id="simple_fin_accounts_data" src="<%= simple_fin_accounts_list_path(accountable_type: @accountable_type) %>">
|
||||||
<div class="text-center text-gray-500 py-8">
|
<div class="text-center text-gray-500 py-8">
|
||||||
<p>No accounts found matching this type from SimpleFIN.</p>
|
<p>Loading accounts...</p>
|
||||||
<p>Please ensure your SimpleFIN subscription is active.</p>
|
<%# Basic SVG Spinner (Tailwind CSS for animation) %>
|
||||||
<%= link_to "Try Again", new_simple_fin_path(accountable_type: @accountable_type), class: "mt-4 inline-block text-primary hover:underline" %>
|
<svg class="animate-spin h-8 w-8 text-primary mx-auto mt-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
</turbo-frame>
|
||||||
<%= styled_form_with url: simple_fin_index_path(accountable_type: @accountable_type), method: :post, data: { turbo: false } do |form| %>
|
|
||||||
<%# Render each account option parsed from SimpleFIN %>
|
|
||||||
<% @simple_fin_accounts.each_with_index do |account, index| %>
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="flex items-start justify-between p-4 hover:bg-surface rounded-lg border border-gray-200 cursor-pointer">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<%= check_box_tag 'selected_account_ids[]', account["id"], class: "border-gray-300 rounded" -%>
|
|
||||||
<div class="flex-grow">
|
|
||||||
<%# Account Name %>
|
|
||||||
<div class="font-medium text-primary"><%= account["name"] %></div>
|
|
||||||
<%# Account Source %>
|
|
||||||
<% if account.dig("org", "name").present? %>
|
|
||||||
<div class="text-xs text-gray-400 mt-0.5">
|
|
||||||
Source:
|
|
||||||
<% if account.dig("org", "url").present? %>
|
|
||||||
<%= link_to account.dig("org", "name"), account.dig("org", "url"), target: "_blank", rel: "noopener noreferrer", class: "hover:underline" %>
|
|
||||||
<% else %>
|
|
||||||
<%= account.dig("org", "name") %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%# SimpleFIN has a tough time determining account types. Let the user fill them out here. %>
|
|
||||||
<div class="mt-2">
|
|
||||||
<% if @accountable_type == "Depository" %>
|
|
||||||
<%= form.select "account[#{account['id']}][subtype]",
|
|
||||||
Depository::SUBTYPES.map { |k, v| [v[:long], k] },
|
|
||||||
{ label: "Subtype", prompt: t("depositories.form.subtype_prompt"), include_blank: t("depositories.form.none") } %>
|
|
||||||
<% elsif @accountable_type == "Investment" %>
|
|
||||||
<%= form.select "account[#{account['id']}][subtype]",
|
|
||||||
Investment::SUBTYPES.map { |k, v| [v[:long], k] },
|
|
||||||
{ label: "Subtype", prompt: t("investments.form.subtype_prompt"), include_blank: t("investments.form.none") } %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%# Current balance %>
|
|
||||||
<% currency_unit = Money::Currency.all_instances.find { |currency| currency.iso_code == account["currency"] }.symbol %>
|
|
||||||
<% balance_color_class = account["balance"].to_f < 0 ? "text-red-500" : "text-green-500" %>
|
|
||||||
<div class="text-sm text-gray-700 font-medium">
|
|
||||||
<span class="<%= balance_color_class %>">
|
|
||||||
<%= number_to_currency(account["balance"], unit: currency_unit || "$") %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
|
||||||
<%= form.submit t("simple_fin.form.add_accounts"), class: "w-full btn btn-primary",
|
|
||||||
data: {
|
|
||||||
disable_with: "Adding Accounts..."
|
|
||||||
} %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
|
@ -1,26 +1,22 @@
|
||||||
<%# locals: (simple_fin_item:) %>
|
<%# locals: (simple_fin_item:) %>
|
||||||
|
|
||||||
<div class="group bg-container p-4 shadow-border-xs rounded-xl">
|
<% if simple_fin_item.accounts.any? %>
|
||||||
<div class="px-4 py-3 sm:px-6 border-b border-gray-200">
|
<div class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||||
<div class="flex items-center justify-between">
|
<div class="px-4 py-3 sm:px-6 border-b border-gray-200">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center justify-between">
|
||||||
<% if simple_fin_item.logo.attached? %>
|
<div class="flex items-center gap-3">
|
||||||
<%= image_tag simple_fin_item.logo, class: "h-8 w-8 rounded-full" %>
|
<h3 >
|
||||||
<% else %>
|
<%= simple_fin_item.institution_name || "SimpleFIN Connection" %>
|
||||||
<div>
|
</h3>
|
||||||
<%= simple_fin_item.institution_name&.first || "?" %>
|
</div>
|
||||||
</div>
|
<%= image_tag "simple-fin-logo.svg", class: "h-6 w-auto", title: "Connected via SimpleFIN" %>
|
||||||
<% end %>
|
|
||||||
<h3 >
|
|
||||||
<%= simple_fin_item.institution_name || "SimpleFIN Connection" %>
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul role="list" class="divide-y divide-gray-200">
|
<ul role="list" class="divide-y divide-gray-200">
|
||||||
<% simple_fin_item.accounts.includes(:accountable, :logo_attachment).active.alphabetically.each do |account| %>
|
<% simple_fin_item.accounts.includes(:accountable, :logo_attachment).active.alphabetically.each do |account| %>
|
||||||
<%= render "accounts/account", account: account %>
|
<%= render "accounts/account", account: account %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
|
@ -5,7 +5,9 @@ en:
|
||||||
add_accounts: "Add Selected Accounts"
|
add_accounts: "Add Selected Accounts"
|
||||||
new:
|
new:
|
||||||
fetch_failed: "Failed to fetch accounts"
|
fetch_failed: "Failed to fetch accounts"
|
||||||
|
rate_limit_hit: "You've exceeded the SimpleFIN Rate limit for today"
|
||||||
create:
|
create:
|
||||||
accounts_created_success: "Accounts Successfully Created"
|
accounts_created_success: "Accounts Successfully Created"
|
||||||
fetch_failed: "Failed to fetch accounts"
|
fetch_failed: "Failed to fetch accounts"
|
||||||
no_acc_selected: "No accounts were selected to add"
|
no_acc_selected: "No accounts were selected to add"
|
||||||
|
rate_limit_hit: "You've exceeded the SimpleFIN Rate limit for today"
|
||||||
|
|
|
@ -201,6 +201,7 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
|
|
||||||
# SimpleFIN routes
|
# SimpleFIN routes
|
||||||
|
get "/simple_fin/accounts_list", to: "simple_fin#accounts_list", as: :simple_fin_accounts_list
|
||||||
resources :simple_fin, only: %i[create new] do
|
resources :simple_fin, only: %i[create new] do
|
||||||
member do
|
member do
|
||||||
post :sync
|
post :sync
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue