mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-02 20:15:22 +02:00
Basic Plaid Integration (#1433)
* Basic plaid data model and linking * Remove institutions, add plaid items * Improve schema and Plaid provider * Add webhook verification sketch * Webhook verification * Item accounts and balances sync setup * Provide test encryption keys * Fix test * Only provide encryption keys in prod * Try defining keys in test env * Consolidate account sync logic * Add back plaid account initialization * Plaid transaction sync * Sync UI overhaul for Plaid * Add liability and investment syncing * Handle investment webhooks and process current day holdings * Remove logs * Remove "all" period select for performance * fix amount calc * Remove todo comment * Coming soon for investment historical data * Document Plaid configuration * Listen for holding updates
This commit is contained in:
parent
3bc9da4105
commit
cbba2ba675
127 changed files with 1537 additions and 841 deletions
|
@ -2,23 +2,25 @@
|
|||
<div class="bg-white p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<%= tag.h2 t(".title"), class: "font-medium text-lg" %>
|
||||
<div data-controller="menu" data-testid="activity-menu">
|
||||
<button class="btn btn--secondary flex items-center gap-2" data-menu-target="button">
|
||||
<%= lucide_icon("plus", class: "w-4 h-4") %>
|
||||
<%= tag.span t(".new") %>
|
||||
</button>
|
||||
<div data-menu-target="content" class="z-10 hidden bg-white rounded-lg border border-alpha-black-25 shadow-xs p-1">
|
||||
<%= link_to new_account_valuation_path(@account), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
|
||||
<%= lucide_icon("circle-dollar-sign", class: "text-gray-500 w-5 h-5") %>
|
||||
<%= tag.span t(".new_balance"), class: "text-sm" %>
|
||||
<% end %>
|
||||
<% unless @account.plaid_account_id.present? %>
|
||||
<div data-controller="menu" data-testid="activity-menu">
|
||||
<button class="btn btn--secondary flex items-center gap-2" data-menu-target="button">
|
||||
<%= lucide_icon("plus", class: "w-4 h-4") %>
|
||||
<%= tag.span t(".new") %>
|
||||
</button>
|
||||
<div data-menu-target="content" class="z-10 hidden bg-white rounded-lg border border-alpha-black-25 shadow-xs p-1">
|
||||
<%= link_to new_account_valuation_path(@account), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
|
||||
<%= lucide_icon("circle-dollar-sign", class: "text-gray-500 w-5 h-5") %>
|
||||
<%= tag.span t(".new_balance"), class: "text-sm" %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to @account.investment? ? new_account_trade_path(@account) : new_transaction_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
|
||||
<%= lucide_icon("credit-card", class: "text-gray-500 w-5 h-5") %>
|
||||
<%= tag.span t(".new_transaction"), class: "text-sm" %>
|
||||
<% end %>
|
||||
<%= link_to @account.investment? ? new_account_trade_path(@account) : new_transaction_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
|
||||
<%= lucide_icon("credit-card", class: "text-gray-500 w-5 h-5") %>
|
||||
<%= tag.span t(".new_transaction"), class: "text-sm" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
|
@ -23,11 +23,6 @@
|
|||
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
|
||||
<% if @holdings.any? %>
|
||||
<%= render partial: "account/holdings/holding", collection: @holdings, spacer_template: "ruler" %>
|
||||
<% elsif @account.needs_sync? || true %>
|
||||
<div class="flex flex-col justify-center items-center pt-4 pb-8">
|
||||
<p class="text-gray-500 p-4"><%= t(".needs_sync") %></p>
|
||||
<%= button_to "Sync holding prices", sync_account_path(@account), class: "bg-gray-900 text-white text-sm rounded-lg px-3 py-2" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-gray-500 text-sm p-4"><%= t(".no_holdings") %></p>
|
||||
<% end %>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<% trade, account = entry.account_trade, entry.account %>
|
||||
|
||||
<div class="grid grid-cols-12 items-center text-gray-900 text-sm font-medium p-4">
|
||||
<div class="pr-10 flex items-center gap-4 col-span-6">
|
||||
<div class="col-span-8 flex items-center gap-4">
|
||||
<% if selectable %>
|
||||
<%= check_box_tag dom_id(entry, "selection"),
|
||||
class: "maybe-checkbox maybe-checkbox--light",
|
||||
|
@ -30,21 +30,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-1 col-span-3">
|
||||
<% if entry.account_transaction? && entry.marked_as_transfer? %>
|
||||
<%= tag.p entry.inflow? ? t(".deposit") : t(".withdrawal") %>
|
||||
<% elsif entry.account_transaction? %>
|
||||
<%= tag.p entry.inflow? ? t(".inflow") : t(".outflow") %>
|
||||
<% else %>
|
||||
<%= tag.p trade.buy? ? t(".buy") : t(".sell") %>
|
||||
<% end %>
|
||||
<div class="col-span-2 justify-self-end font-medium text-sm">
|
||||
<%= tag.span format_money(entry.amount_money) %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3 flex items-center justify-end">
|
||||
<% if entry.account_transaction? %>
|
||||
<%= tag.p format_money(entry.amount_money * -1), class: { "text-green-500": entry.inflow? } %>
|
||||
<% else %>
|
||||
<%= tag.p format_money(entry.amount_money * -1), class: { "text-green-500": trade.sell? } %>
|
||||
<% end %>
|
||||
<div class="col-span-2 justify-self-end">
|
||||
<%= tag.p "--", class: "font-medium text-sm text-gray-400" %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -68,7 +68,11 @@
|
|||
|
||||
<% if show_balance %>
|
||||
<div class="col-span-2 justify-self-end">
|
||||
<%= tag.p format_money(entry.trend.current), class: "font-medium text-sm text-gray-900" %>
|
||||
<% if entry.account.investment? %>
|
||||
<%= tag.p "--", class: "font-medium text-sm text-gray-400" %>
|
||||
<% else %>
|
||||
<%= tag.p format_money(entry.trend.current), class: "font-medium text-sm text-gray-900" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
</section>
|
||||
|
||||
<section class="space-y-2">
|
||||
<%= f.collection_select :from_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".from") }, required: true %>
|
||||
<%= f.collection_select :to_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".to") }, required: true %>
|
||||
<%= f.collection_select :from_account_id, Current.family.accounts.manual.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".from") }, required: true %>
|
||||
<%= f.collection_select :to_account_id, Current.family.accounts.manual.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".to") }, required: true %>
|
||||
<%= f.money_field :amount, label: t(".amount"), required: true, hide_currency: true %>
|
||||
<%= f.date_field :date, value: transfer.date, label: t(".date"), required: true, max: Date.current %>
|
||||
</section>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<%# locals: (accountable:) %>
|
||||
|
||||
<%= link_to new_polymorphic_path(accountable, institution_id: params[:institution_id], step: "method_select", return_to: params[:return_to]),
|
||||
<%= link_to new_polymorphic_path(accountable, step: "method_select", return_to: params[:return_to]),
|
||||
class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-alpha-black-25 hover:bg-alpha-black-25 border border-transparent block px-2 rounded-lg p-2" do %>
|
||||
<span style="background-color: color-mix(in srgb, <%= accountable.color %> 10%, white);" class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg border border-alpha-black-25">
|
||||
<%= lucide_icon(accountable.icon, style: "color: #{accountable.color}", class: "w-5 h-5") %>
|
||||
|
|
|
@ -5,12 +5,6 @@
|
|||
<%= form.hidden_field :accountable_type %>
|
||||
<%= form.hidden_field :return_to, value: params[:return_to] %>
|
||||
|
||||
<% if account.new_record? %>
|
||||
<%= form.hidden_field :institution_id %>
|
||||
<% else %>
|
||||
<%= form.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %>
|
||||
<% end %>
|
||||
|
||||
<%= form.text_field :name, placeholder: t(".name_placeholder"), required: "required", label: t(".name_label") %>
|
||||
<%= form.money_field :balance, label: t(".balance"), required: true, default_currency: Current.family.currency %>
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<%= button_to sync_all_accounts_path, class: "btn btn--outline flex items-center gap-2", title: t(".sync") do %>
|
||||
<%= lucide_icon "refresh-cw", class: "w-5 h-5" %>
|
||||
<span><%= t(".sync") %></span>
|
||||
<% end %>
|
|
@ -7,19 +7,14 @@
|
|||
<h1 class="text-xl"><%= t(".accounts") %></h1>
|
||||
<div class="flex items-center gap-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= link_to new_institution_path,
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg font-normal",
|
||||
data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon "building-2", class: "w-5 h-5 text-gray-500" %>
|
||||
<span class="text-black"><%= t(".add_institution") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= button_to sync_all_accounts_path,
|
||||
disabled: Current.family.syncing?,
|
||||
class: "btn btn--outline flex items-center gap-2",
|
||||
title: t(".sync") do %>
|
||||
<%= lucide_icon "refresh-cw", class: "w-5 h-5" %>
|
||||
<span><%= t(".sync") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= render "sync_all_button" %>
|
||||
|
||||
<%= link_to new_account_path(return_to: accounts_path),
|
||||
data: { turbo_frame: "modal" },
|
||||
class: "btn btn--primary flex items-center gap-1" do %>
|
||||
|
@ -30,16 +25,16 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<% if @accounts.empty? && @institutions.empty? %>
|
||||
<% if @manual_accounts.empty? && @plaid_items.empty? %>
|
||||
<%= render "empty" %>
|
||||
<% else %>
|
||||
<div class="space-y-2">
|
||||
<% @institutions.each do |institution| %>
|
||||
<%= render "accounts/index/institution_accounts", institution: %>
|
||||
<% if @plaid_items.any? %>
|
||||
<%= render @plaid_items.sort_by(&:created_at) %>
|
||||
<% end %>
|
||||
|
||||
<% if @accounts.any? %>
|
||||
<%= render "accounts/index/institutionless_accounts", accounts: @accounts %>
|
||||
<% if @manual_accounts.any? %>
|
||||
<%= render "accounts/index/manual_accounts", accounts: @manual_accounts %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<%# locals: (accounts:) %>
|
||||
|
||||
<% accounts.group_by(&:accountable_type).each do |group, accounts| %>
|
||||
<% accounts.group_by(&:accountable_type).sort_by { |group, _| group }.each do |group, accounts| %>
|
||||
<div class="bg-gray-25 p-1 rounded-xl">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-medium text-gray-500">
|
||||
<p><%= to_accountable_title(Accountable.from_type(group)) %></p>
|
|
@ -1,91 +0,0 @@
|
|||
<%# locals: (institution:) %>
|
||||
|
||||
<details open class="group bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2 focus-visible:outline-none">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-gray-500 w-5" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full bg-black/5">
|
||||
<% if institution_logo(institution) %>
|
||||
<%= image_tag institution_logo(institution), class: "rounded-full h-full w-full" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p institution.name.first.upcase, class: "text-blue-600 text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<%= link_to institution.name, edit_institution_path(institution), data: { turbo_frame: :modal }, class: "font-medium text-gray-900 hover:underline" %>
|
||||
<% if institution.has_issues? %>
|
||||
<div class="flex items-center gap-1 text-error">
|
||||
<%= lucide_icon "alert-octagon", class: "shrink-0 w-4 h-4" %>
|
||||
<%= tag.span t(".has_issues") %>
|
||||
</div>
|
||||
<% elsif institution.syncing? %>
|
||||
<div class="text-gray-500 flex items-center gap-1">
|
||||
<%= lucide_icon "loader", class: "w-4 h-4 animate-pulse" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-gray-500"><%= institution.last_synced_at ? t(".status", last_synced_at: time_ago_in_words(institution.last_synced_at)) : t(".status_never") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= button_to sync_institution_path(institution), method: :post, class: "text-gray-900 flex hover:text-gray-800 items-center text-sm font-medium hover:underline" do %>
|
||||
<%= lucide_icon "refresh-cw", class: "w-4 h-4" %>
|
||||
<% end %>
|
||||
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= link_to new_account_path(institution_id: institution.id, return_to: accounts_path),
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg",
|
||||
data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon "plus", class: "w-5 h-5 text-gray-500" %>
|
||||
|
||||
<span><%= t(".add_account_to_institution") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= link_to edit_institution_path(institution),
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg",
|
||||
data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
|
||||
|
||||
<span><%= t(".edit") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= button_to institution_path(institution),
|
||||
method: :delete,
|
||||
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
|
||||
data: {
|
||||
turbo_confirm: {
|
||||
title: t(".confirm_title"),
|
||||
body: t(".confirm_body"),
|
||||
accept: t(".confirm_accept")
|
||||
}
|
||||
} do %>
|
||||
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
|
||||
|
||||
<span><%= t(".delete") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<div class="space-y-4 mt-4">
|
||||
<% if institution.accounts.any? %>
|
||||
<%= render "accountable_group", accounts: institution.accounts %>
|
||||
<% else %>
|
||||
<div class="p-4 flex flex-col gap-3 items-center justify-center">
|
||||
<p class="text-gray-500 text-sm">There are no accounts in this financial institution</p>
|
||||
<%= link_to new_account_path(institution_id: institution.id, return_to: accounts_path), class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-1.5 pr-2", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-4 h-4") %>
|
||||
<span><%= t(".new_account") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
|
@ -12,6 +12,6 @@
|
|||
</summary>
|
||||
|
||||
<div class="space-y-4 mt-4">
|
||||
<%= render "accountable_group", accounts: accounts %>
|
||||
<%= render "accounts/index/account_groups", accounts: accounts %>
|
||||
</div>
|
||||
</details>
|
|
@ -1,4 +1,4 @@
|
|||
<%# locals: (path:) %>
|
||||
<%# locals: (path:, link_token: nil) %>
|
||||
|
||||
<%= render layout: "accounts/new/container", locals: { title: t(".title"), back_path: new_account_path } do %>
|
||||
<div class="text-sm">
|
||||
|
@ -9,11 +9,13 @@
|
|||
<%= t("accounts.new.method_selector.manual_entry") %>
|
||||
<% end %>
|
||||
|
||||
<span class="flex items-center w-full gap-4 p-2 px-2 text-center border border-transparent rounded-lg cursor-not-allowed focus:outline-none focus:bg-gray-50 focus:border focus:border-gray-200 text-gray-400">
|
||||
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
|
||||
<%= lucide_icon("link-2", class: "text-gray-500 w-5 h-5") %>
|
||||
</span>
|
||||
<%= t("accounts.new.method_selector.connected_entry") %>
|
||||
</span>
|
||||
<% if link_token.present? %>
|
||||
<button data-controller="plaid" data-action="plaid#open modal#close" data-plaid-link-token-value="<%= @link_token %>" class="flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2">
|
||||
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
|
||||
<%= lucide_icon("link-2", class: "text-gray-500 w-5 h-5") %>
|
||||
</span>
|
||||
<%= t("accounts.new.method_selector.connected_entry") %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -20,8 +20,10 @@
|
|||
<% end %>
|
||||
|
||||
<div class="flex items-center gap-3 ml-auto">
|
||||
<%= 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-500 hover:text-gray-400" %>
|
||||
<% unless account.plaid_account_id.present? %>
|
||||
<%= button_to sync_account_path(account), disabled: account.syncing?, class: "flex items-center gap-2", title: "Sync Account" do %>
|
||||
<%= lucide_icon "refresh-cw", class: "w-4 h-4 text-gray-500 hover:text-gray-400" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= render "accounts/show/menu", account: account %>
|
||||
|
|
|
@ -2,23 +2,32 @@
|
|||
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= link_to edit_account_path(account),
|
||||
<% if account.plaid_account_id.present? %>
|
||||
<%= link_to accounts_path,
|
||||
data: { turbo_frame: :_top },
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg" do %>
|
||||
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
|
||||
|
||||
<span><%= t(".manage") %></span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to edit_account_path(account),
|
||||
data: { turbo_frame: :modal },
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg" do %>
|
||||
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
|
||||
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
|
||||
|
||||
<span><%= t(".edit") %></span>
|
||||
<% end %>
|
||||
<span><%= t(".edit") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_import_path,
|
||||
<%= link_to new_import_path,
|
||||
data: { turbo_frame: :modal },
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg" do %>
|
||||
<%= lucide_icon "download", class: "w-5 h-5 text-gray-500" %>
|
||||
<%= lucide_icon "download", class: "w-5 h-5 text-gray-500" %>
|
||||
|
||||
<span><%= t(".import") %></span>
|
||||
<% end %>
|
||||
<span><%= t(".import") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= button_to account_path(account),
|
||||
<%= button_to account_path(account),
|
||||
method: :delete,
|
||||
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
|
||||
data: {
|
||||
|
@ -29,7 +38,8 @@
|
|||
accept: t(".confirm_accept", name: account.name)
|
||||
}
|
||||
} do %>
|
||||
<%= lucide_icon("trash-2", class: "w-5 h-5 mr-2") %> Delete account
|
||||
<%= lucide_icon("trash-2", class: "w-5 h-5 mr-2") %> Delete account
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<% if params[:step] == "method_select" %>
|
||||
<%= render "accounts/new/method_selector", path: new_credit_card_path(institution_id: params[:institution_id], return_to: params[:return_to]) %>
|
||||
<%= render "accounts/new/method_selector", path: new_credit_card_path(return_to: params[:return_to]), link_token: @link_token %>
|
||||
<% else %>
|
||||
<%= modal_form_wrapper title: t(".title") do %>
|
||||
<%= render "credit_cards/form", account: @account, url: credit_cards_path %>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<% if params[:step] == "method_select" %>
|
||||
<%= render "accounts/new/method_selector", path: new_crypto_path(institution_id: params[:institution_id], return_to: params[:return_to]) %>
|
||||
<%= render "accounts/new/method_selector", path: new_crypto_path(return_to: params[:return_to]), link_token: @link_token %>
|
||||
<% else %>
|
||||
<%= modal_form_wrapper title: t(".title") do %>
|
||||
<%= render "cryptos/form", account: @account, url: cryptos_path %>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<% if params[:step] == "method_select" %>
|
||||
<%= render "accounts/new/method_selector", path: new_depository_path(institution_id: params[:institution_id], return_to: params[:return_to]) %>
|
||||
<%= render "accounts/new/method_selector", path: new_depository_path(return_to: params[:return_to]), link_token: @link_token %>
|
||||
<% else %>
|
||||
<%= modal_form_wrapper title: t(".title") do %>
|
||||
<%= render "depositories/form", account: @account, url: depositories_path %>
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
<%= styled_form_with model: institution, class: "space-y-4", data: { turbo_frame: "_top", controller: "profile-image-preview" } do |f| %>
|
||||
<div class="flex justify-center items-center py-4">
|
||||
<%= f.label :logo do %>
|
||||
<div class="relative cursor-pointer hover:opacity-80 w-16 h-16 rounded-full bg-gray-50">
|
||||
<% persisted_logo = institution_logo(institution) %>
|
||||
|
||||
<% if persisted_logo %>
|
||||
<%= image_tag persisted_logo, class: "absolute inset-0 rounded-full w-full h-full object-cover" %>
|
||||
<% end %>
|
||||
|
||||
<div data-profile-image-preview-target="imagePreview" class="absolute inset-0 h-full w-full flex items-center justify-center">
|
||||
<% unless persisted_logo %>
|
||||
<%= lucide_icon "image-plus", class: "w-5 h-5 text-gray-500 cursor-pointer", data: { profile_image_preview_target: "template" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= f.file_field :logo,
|
||||
accept: "image/png, image/jpeg",
|
||||
class: "hidden",
|
||||
data: { profile_image_preview_target: "fileField", action: "profile-image-preview#preview" } %>
|
||||
<%= f.text_field :name, label: t(".name") %>
|
||||
<%= f.submit %>
|
||||
<% end %>
|
|
@ -1,3 +0,0 @@
|
|||
<%= modal_form_wrapper title: t(".edit", institution: @institution.name) do %>
|
||||
<%= render "form", institution: @institution %>
|
||||
<% end %>
|
|
@ -1,3 +0,0 @@
|
|||
<%= modal_form_wrapper title: t(".new_institution") do %>
|
||||
<%= render "form", institution: @institution %>
|
||||
<% end %>
|
|
@ -1 +1,25 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<% period = Period.from_param(params[:period]) %>
|
||||
<% series = account.series(period: period) %>
|
||||
<% trend = series.trend %>
|
||||
|
||||
<div class="bg-white shadow-xs rounded-xl border border-alpha-black-25 rounded-lg">
|
||||
<div class="p-4 flex justify-between">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<%= tag.p t(".value"), class: "text-sm font-medium text-gray-500" %>
|
||||
</div>
|
||||
|
||||
<%= tag.p format_money(account.value), class: "text-gray-900 text-3xl font-medium" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative h-64 flex items-center justify-center">
|
||||
<%= image_tag "placeholder-graph.svg", class: "w-full h-full object-cover rounded-bl-lg rounded-br-lg opacity-50" %>
|
||||
<div class="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center space-y-1">
|
||||
<p class="text-gray-900 text-sm">Historical investment data coming soon.</p>
|
||||
<p class="text-gray-500 text-sm">We're working to bring you the full picture.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<% if params[:step] == "method_select" %>
|
||||
<%= render "accounts/new/method_selector", path: new_investment_path(institution_id: params[:institution_id], return_to: params[:return_to]) %>
|
||||
<%= render "accounts/new/method_selector", path: new_investment_path(return_to: params[:return_to]), link_token: @link_token %>
|
||||
<% else %>
|
||||
<%= modal_form_wrapper title: t(".title") do %>
|
||||
<%= render "investments/form", account: @account, url: investments_path %>
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
<%= tag.div class: "space-y-4" do %>
|
||||
<%= render "accounts/show/header", account: @account %>
|
||||
|
||||
<%= render "accounts/show/chart",
|
||||
<% if @account.plaid_account_id.present? %>
|
||||
<%= render "investments/chart", account: @account %>
|
||||
<% else %>
|
||||
<%= render "accounts/show/chart",
|
||||
account: @account,
|
||||
title: t(".chart_title"),
|
||||
tooltip: render(
|
||||
|
@ -12,6 +15,7 @@
|
|||
value: @account.value,
|
||||
cash: @account.balance_money
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<%= render "accounts/show/tabs", account: @account, tabs: [
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
|
||||
<%= javascript_include_tag "https://cdn.plaid.com/link/v2/stable/link-initialize.js" %>
|
||||
<%= combobox_style_tag %>
|
||||
|
||||
<%= javascript_importmap_tags %>
|
||||
|
@ -30,9 +31,13 @@
|
|||
<%= render "impersonation_sessions/super_admin_bar" if Current.true_user&.super_admin? %>
|
||||
<%= render "impersonation_sessions/approval_bar" if Current.true_user&.impersonated_support_sessions&.initiated&.any? %>
|
||||
|
||||
<div class="fixed z-50 space-y-1 top-6 right-10">
|
||||
<div id="notification-tray">
|
||||
<div class="fixed z-50 bottom-6 left-6">
|
||||
<div id="notification-tray" class="space-y-1">
|
||||
<%= render_flash_notifications %>
|
||||
|
||||
<% if Current.family&.syncing? %>
|
||||
<%= render "shared/notification", id: "syncing-notification", type: :processing, message: t(".syncing") %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<% if params[:step] == "method_select" %>
|
||||
<%= render "accounts/new/method_selector", path: new_loan_path(institution_id: params[:institution_id], return_to: params[:return_to]) %>
|
||||
<%= render "accounts/new/method_selector", path: new_loan_path(return_to: params[:return_to]), link_token: @link_token %>
|
||||
<% else %>
|
||||
<%= modal_form_wrapper title: t(".title") do %>
|
||||
<%= render "loans/form", account: @account, url: loans_path %>
|
||||
|
|
76
app/views/plaid_items/_plaid_item.html.erb
Normal file
76
app/views/plaid_items/_plaid_item.html.erb
Normal file
|
@ -0,0 +1,76 @@
|
|||
<%# locals: (plaid_item:) %>
|
||||
|
||||
<%= tag.div id: dom_id(plaid_item) do %>
|
||||
<details open class="group bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2 focus-visible:outline-none">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-gray-500 w-5" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full bg-black/5">
|
||||
<% if plaid_item.logo.attached? %>
|
||||
<%= image_tag plaid_item.logo, class: "rounded-full h-full w-full" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p plaid_item.name.first.upcase, class: "text-blue-600 text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<%= tag.p plaid_item.name, class: "font-medium text-gray-900" %>
|
||||
<% if plaid_item.syncing? %>
|
||||
<div class="text-gray-500 flex items-center gap-1">
|
||||
<%= lucide_icon "loader", class: "w-4 h-4 animate-pulse" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif plaid_item.sync_error.present? %>
|
||||
<div class="text-gray-500 flex items-center gap-1">
|
||||
<%= lucide_icon "alert-circle", class: "w-4 h-4 text-red-500" %>
|
||||
<%= tag.span t(".error"), class: "text-red-500" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-gray-500">
|
||||
<%= plaid_item.last_synced_at ? t(".status", timestamp: time_ago_in_words(plaid_item.last_synced_at)) : t(".status_never") %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= button_to sync_plaid_item_path(plaid_item), disabled: plaid_item.syncing?, class: "disabled:text-gray-400 text-gray-900 flex hover:text-gray-800 items-center text-sm font-medium hover:underline" do %>
|
||||
<%= lucide_icon "refresh-cw", class: "w-4 h-4" %>
|
||||
<% end %>
|
||||
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= button_to plaid_item_path(plaid_item),
|
||||
method: :delete,
|
||||
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
|
||||
data: {
|
||||
turbo_confirm: {
|
||||
title: t(".confirm_title"),
|
||||
body: t(".confirm_body"),
|
||||
accept: t(".confirm_accept")
|
||||
}
|
||||
} do %>
|
||||
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
|
||||
|
||||
<span><%= t(".delete") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<div class="space-y-4 mt-4">
|
||||
<% if plaid_item.accounts.any? %>
|
||||
<%= render "accounts/index/account_groups", accounts: plaid_item.accounts %>
|
||||
<% else %>
|
||||
<div class="p-4 flex flex-col gap-3 items-center justify-center">
|
||||
<p class="text-gray-900 font-medium text-sm"><%= t(".no_accounts_title") %></p>
|
||||
<p class="text-gray-500 text-sm"><%= t(".no_accounts_description") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
<% end %>
|
|
@ -4,7 +4,7 @@
|
|||
<% action = "animationend->element-removal#remove" if type == :notice %>
|
||||
|
||||
<%= tag.div class: "flex gap-3 rounded-lg border bg-white p-4 group max-w-80 shadow-xs border-alpha-black-25",
|
||||
id: id,
|
||||
id: type == :processing ? "syncing-notification" : id,
|
||||
data: {
|
||||
controller: "element-removal",
|
||||
action: action
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<section class="space-y-2 overflow-hidden">
|
||||
<%= f.text_field :name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %>
|
||||
<%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") }, required: true, class: "form-field__input text-ellipsis" %>
|
||||
<%= f.collection_select :account_id, Current.family.accounts.manual.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") }, required: true, class: "form-field__input text-ellipsis" %>
|
||||
<%= f.money_field :amount, label: t(".amount"), required: true %>
|
||||
<%= f.hidden_field :entryable_type, value: "Account::Transaction" %>
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue