1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-03 04:25:21 +02:00

Multi-Currency Part 2 (#543)

* Support all currencies, handle outside DB

* Remove currencies from seed

* Fix account balance namespace

* Set default currency on authentication

* Cache currency instances

* Implement multi-currency syncs with tests

* Series fallback, passing tests

* Fix conflicts

* Make value group concrete class that works with currency values

* Fix migration conflict

* Update tests to expect multi-currency results

* Update account list to use group method

* Namespace updates

* Fetch unknown exchange rates from API

* Fix date range bug

* Ensure demo data works without external API

* Enforce cascades only at DB level
This commit is contained in:
Zach Gollwitzer 2024-03-21 13:39:10 -04:00 committed by GitHub
parent de0cba9fed
commit 110855d077
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 1226 additions and 714 deletions

View file

@ -1,22 +1,24 @@
<%# locals: (type:) -%>
<% accounts = Current.family.accounts.where(accountable_type: type.name) %>
<% if accounts.sum(&:converted_balance) > 0 %>
<%# locals: (group:) -%>
<% type = Accountable.from_type(group.name) %>
<% if group %>
<details class="mb-1 text-sm group" data-controller="account-collapse" data-account-collapse-type-value="<%= type %>">
<summary class="flex gap-4 px-2 py-3 items-center w-full rounded-[10px] font-medium hover:bg-gray-100">
<summary class="flex gap-4 px-3 py-2 items-center w-full rounded-[10px] font-medium hover:bg-gray-100">
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
<div class="text-left"><%= type.model_name.human %></div>
<div class="ml-auto"><%= format_money accounts.sum(&:converted_balance) %></div>
<div class="ml-auto flex flex-col items-end">
<p class="text-right"><%= format_money group.sum %></p>
</div>
</summary>
<% accounts.each do |account| %>
<%= link_to account_path(account), class: "flex items-center w-full gap-3 px-2 py-3 mb-1 hover:bg-gray-100 rounded-[10px]" do %>
<% group.children.each do |account_value_node| %>
<%= link_to account_path(account_value_node.original), class: "flex items-center w-full gap-3 px-2 py-3 mb-1 hover:bg-gray-100 rounded-[10px]" do %>
<div>
<p class="font-medium"><%= account.name %></p>
<% if account.subtype %>
<p class="text-xs text-gray-500"><%= account.subtype&.humanize %></p>
<p class="font-medium"><%= account_value_node.name %></p>
<% if account_value_node.original.subtype %>
<p class="text-xs text-gray-500"><%= account_value_node.original.subtype&.humanize %></p>
<% end %>
</div>
<p class="ml-auto font-medium"><%= format_money account.converted_balance %></p>
<p class="ml-auto font-medium"><%= format_money account_value_node.original.balance_money %></p>
<% end %>
<% end %>
<%= link_to new_account_path(step: 'method', type: type.name.demodulize), class: "flex items-center gap-4 px-2 py-3 mb-1 text-gray-500 text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %>

View file

@ -8,6 +8,9 @@
<h2 class="font-medium text-xl"><%= @account.name %></h2>
</div>
<div class="flex items-center gap-3">
<%= button_to sync_account_path(@account), method: :post, class: "flex items-center gap-2", title: "Sync Account" do %>
<%= lucide_icon "refresh-cw", class: "w-4 h-4 text-gray-900 hover:text-gray-500" %>
<% end %>
<div class="relative cursor-not-allowed">
<div class="flex items-center gap-2 px-3 py-2">
<span class="text-gray-900"><%= @account.balance_money.currency.iso_code %> <%= @account.balance_money.currency.symbol %></span>

View file

@ -8,7 +8,6 @@
<meta name="apple-mobile-web-app-title" content="Maybe">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
@ -16,7 +15,6 @@
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#ffffff">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="theme-color" content="#ffffff">
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
@ -71,8 +69,8 @@
<%= lucide_icon("plus", class: "w-5 h-5") %>
<p><%= t('.new_account') %></p>
<% end %>
<% Accountable.types.each do |type| %>
<%= render 'accounts/account_list', type: Accountable.from_type(type) %>
<% account_groups.each do |group| %>
<%= render 'accounts/account_list', group: group %>
<% end %>
</div>
</div>

View file

@ -1,47 +1,47 @@
<%# locals: (account_group:) %>
<% text_class = accountable_text_class(account_group.name) %>
<%# locals: (accountable_group:) %>
<% text_class = accountable_text_class(accountable_group.name) %>
<details class="open:bg-gray-25 group">
<summary class="flex p-4 items-center w-full rounded-lg font-medium hover:bg-gray-50 text-gray-500 text-sm font-medium cursor-pointer">
<%= lucide_icon("chevron-down", class: "hidden group-open:block w-5 h-5") %>
<%= lucide_icon("chevron-right", class: "group-open:hidden w-5 h-5") %>
<div class="ml-4 h-2.5 w-2.5 rounded-full <%= accountable_bg_class(account_group.name) %>"></div>
<p class="text-gray-900 ml-2"><%= to_accountable_title(Accountable.from_type(account_group.name)) %></p>
<div class="ml-4 h-2.5 w-2.5 rounded-full <%= accountable_bg_class(accountable_group.name) %>"></div>
<p class="text-gray-900 ml-2"><%= to_accountable_title(Accountable.from_type(accountable_group.name)) %></p>
<span class="mx-1">&middot;</span>
<div ><%= account_group.children.count %></div>
<div ><%= accountable_group.children.count %></div>
<div class="ml-auto text-right flex items-center gap-10 text-sm font-medium text-gray-900">
<div class="flex items-center justify-end gap-2 w-24">
<%= render partial: "shared/progress_circle", locals: { progress: account_group.percent_of_total, text_class: text_class } %>
<p><%= account_group.percent_of_total.round(1) %>%</p>
<%= render partial: "shared/progress_circle", locals: { progress: accountable_group.percent_of_total, text_class: text_class } %>
<p><%= accountable_group.percent_of_total.round(1) %>%</p>
</div>
<div class="w-24">
<p><%= format_money account_group.sum %></p>
<p><%= format_money accountable_group.sum %></p>
</div>
<div class="w-40">
<%= render partial: "shared/trend_change", locals: { trend: account_group.series.trend } %>
<%= render partial: "shared/trend_change", locals: { trend: accountable_group.series.trend } %>
</div>
</div>
</summary>
<div class="px-4 py-3 space-y-4">
<% account_group.children.map do |account| %>
<% accountable_group.children.map do |account_value_node| %>
<div class="flex items-center justify-between text-sm font-medium text-gray-900">
<div class="flex items-center gap-4">
<div class="flex items-center justify-center w-8 h-8 rounded-full <%= text_class %> <%= accountable_bg_transparent_class(account_group.name) %>">
<%= account.name[0].upcase %>
<div class="flex items-center justify-center w-8 h-8 rounded-full <%= text_class %> <%= accountable_bg_transparent_class(account_value_node.name) %>">
<%= account_value_node.name[0].upcase %>
</div>
<div>
<p><%= account.name %></p>
<p><%= account_value_node.name %></p>
</div>
</div>
<div class="flex gap-10 items-center text-right">
<div class="flex items-center justify-end gap-2 w-24">
<%= render partial: "shared/progress_circle", locals: { progress: account.percent_of_total, text_class: text_class } %>
<p><%= account.percent_of_total %>%</p>
<%= render partial: "shared/progress_circle", locals: { progress: account_value_node.percent_of_total, text_class: text_class } %>
<p><%= account_value_node.percent_of_total %>%</p>
</div>
<div class="w-24">
<p><%= format_money account.sum %></p>
<p><%= format_money account_value_node.original.balance_money %></p>
</div>
<div class="w-40">
<%= render partial: "shared/trend_change", locals: { trend: account.series.trend } %>
<%= render partial: "shared/trend_change", locals: { trend: account_value_node.original.series.trend } %>
</div>
</div>
</div>

View file

@ -15,6 +15,6 @@
</div>
</div>
<div class="bg-white border border-alpha-black-25 shadow-xs rounded-lg divide-y divide-alpha-black-50">
<%= render partial: "account_group_disclosure", collection: account_groups, as: :account_group %>
<%= render partial: "account_group_disclosure", collection: account_groups, as: :accountable_group %>
</div>
</div>

View file

@ -10,7 +10,7 @@
<%= render partial: "shared/balance_heading", locals: {
label: "Net Worth",
period: @period,
balance: Current.family.net_worth_money,
balance: Current.family.net_worth,
trend: @net_worth_series.trend
}
%>
@ -26,7 +26,7 @@
<%= render partial: "shared/balance_heading", locals: {
label: "Assets",
period: @period,
balance: Current.family.assets_money,
balance: Current.family.assets,
trend: @asset_series.trend
} %>
</div>
@ -44,7 +44,7 @@
label: "Liabilities",
period: @period,
size: "md",
balance: Current.family.liabilities_money,
balance: Current.family.liabilities,
trend: @liability_series.trend
} %>
</div>

View file

@ -1,22 +1,14 @@
<h1 class="text-3xl font-semibold font-display">Update settings</h1>
<%= form_with model: Current.user, url: settings_path, html: { class: "space-y-4" } do |form| %>
<%= form.fields_for :family_attributes do |family_fields| %>
<%= family_fields.text_field :name, placeholder: "Family name", value: Current.family.name, label: "Family name" %>
<%= family_fields.select :currency, options_for_select(Currency.all.order(iso_code: :asc).map { |currency| ["#{currency.iso_code} (#{currency.name})", currency.iso_code] }, selected: Current.family.currency), { label: "Currency" } %>
<%= family_fields.select :currency, options_for_select(Money::Currency.popular.map { |currency| ["#{currency.iso_code} (#{currency.name})", currency.iso_code] }, selected: Current.family.currency), { label: "Currency" } %>
<% end %>
<%= form.text_field :first_name, placeholder: "First name", value: Current.user.first_name, label: true %>
<%= form.text_field :last_name, placeholder: "Last name", value: Current.user.last_name, label: true %>
<%= form.email_field :email, placeholder: "Email", value: Current.user.email, label: true %>
<%= form.password_field :password, label: true %>
<%= form.password_field :password_confirmation, label: true %>
<div class="fixed right-5 bottom-5">
<button type="submit" class="flex items-center justify-center w-12 h-12 mb-2 bg-black rounded-full shrink-0 grow-0 hover:bg-gray-600">
<%= inline_svg_tag('icn-check.svg', class: 'text-white fill-current') %>