Add SimpleFin user interface components

- Create SimpleFin connection management views
- Add account setup modal with type selection
- Include connection form with token input and instructions
- Update accounts index to display SimpleFin items
- Add SimpleFin option to account method selector
- Include SimpleFin in settings navigation
This commit is contained in:
Sholom Ber
2025-08-07 12:40:14 -04:00
parent 18b69a490c
commit 35693e51f0
7 changed files with 276 additions and 1 deletions

View File

@@ -21,7 +21,7 @@
</div>
</header>
<% if @manual_accounts.empty? && @plaid_items.empty? %>
<% if @manual_accounts.empty? && @plaid_items.empty? && @simplefin_items.empty? %>
<%= render "empty" %>
<% else %>
<div class="space-y-2">
@@ -29,6 +29,10 @@
<%= render @plaid_items.sort_by(&:created_at) %>
<% end %>
<% if @simplefin_items.any? %>
<%= render @simplefin_items.sort_by(&:created_at) %>
<% end %>
<% if @manual_accounts.any? %>
<%= render "accounts/index/manual_accounts", accounts: @manual_accounts %>
<% end %>

View File

@@ -32,5 +32,15 @@
<%= t("accounts.new.method_selector.connected_entry_eu") %>
<% end %>
<% end %>
<%# SimpleFin Link %>
<%= link_to new_simplefin_item_path,
class: "text-primary flex items-center gap-4 w-full text-center focus:outline-hidden focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2",
data: { turbo_frame: "modal" } do %>
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-green-100 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
<%= icon("building-2", class: "text-green-600") %>
</span>
Connect with SimpleFin
<% end %>
</div>
<% end %>

View File

@@ -10,6 +10,7 @@ nav_sections = [
{ label: t(".self_hosting_label"), path: settings_hosting_path, icon: "database", if: self_hosted? },
{ label: t(".billing_label"), path: settings_billing_path, icon: "circle-dollar-sign", if: !self_hosted? },
{ label: t(".accounts_label"), path: accounts_path, icon: "layers" },
{ label: "SimpleFin", path: simplefin_items_path, icon: "building-2" },
{ label: t(".imports_label"), path: imports_path, icon: "download" }
]
},

View File

@@ -0,0 +1,95 @@
<%# locals: (simplefin_item:) %>
<%= tag.div id: dom_id(simplefin_item) do %>
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
<summary class="flex items-center justify-between gap-2 focus-visible:outline-hidden">
<div class="flex items-center gap-2">
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
<div class="flex items-center justify-center h-8 w-8 bg-green-600/10 rounded-full">
<% if simplefin_item.logo.attached? %>
<%= image_tag simplefin_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
<% else %>
<div class="flex items-center justify-center">
<%= tag.p simplefin_item.name.first.upcase, class: "text-green-600 text-xs font-medium" %>
</div>
<% end %>
</div>
<div class="pl-1 text-sm">
<div class="flex items-center gap-2">
<%= tag.p simplefin_item.name, class: "font-medium text-primary" %>
<% if simplefin_item.scheduled_for_deletion? %>
<p class="text-destructive text-sm animate-pulse">(deletion in progress...)</p>
<% end %>
</div>
<% if simplefin_item.syncing? %>
<div class="text-secondary flex items-center gap-1">
<%= icon "loader", size: "sm", class: "animate-pulse" %>
<%= tag.span "Syncing..." %>
</div>
<% elsif simplefin_item.requires_update? %>
<div class="text-warning flex items-center gap-1">
<%= icon "alert-triangle", size: "sm", color: "warning" %>
<%= tag.span "Requires Update" %>
</div>
<% elsif simplefin_item.sync_error.present? %>
<div class="text-secondary flex items-center gap-1">
<%= icon "alert-circle", size: "sm", color: "destructive" %>
<%= tag.span "Error", class: "text-destructive" %>
</div>
<% else %>
<p class="text-secondary">
<%= simplefin_item.last_synced_at ? "Last synced #{time_ago_in_words(simplefin_item.last_synced_at)} ago" : "Never synced" %>
</p>
<% end %>
</div>
</div>
<div class="flex items-center gap-2">
<% if Rails.env.development? %>
<%= icon(
"refresh-cw",
as_button: true,
href: sync_simplefin_item_path(simplefin_item)
) %>
<% end %>
<%= render DS::Menu.new do |menu| %>
<% menu.with_item(
variant: "button",
text: "Delete",
icon: "trash-2",
href: simplefin_item_path(simplefin_item),
method: :delete,
confirm: CustomConfirm.for_resource_deletion(simplefin_item.name, high_severity: true)
) %>
<% end %>
</div>
</summary>
<% unless simplefin_item.scheduled_for_deletion? %>
<div class="space-y-4 mt-4">
<% if simplefin_item.accounts.any? %>
<%= render "accounts/index/account_groups", accounts: simplefin_item.accounts %>
<% elsif simplefin_item.pending_account_setup? %>
<div class="p-4 flex flex-col gap-3 items-center justify-center">
<p class="text-primary font-medium text-sm">Accounts ready to set up</p>
<p class="text-secondary text-sm">Choose account types for your imported SimpleFin accounts.</p>
<%= render DS::Link.new(
text: "Set Up Accounts",
icon: "settings",
variant: "primary",
href: setup_accounts_simplefin_item_path(simplefin_item)
) %>
</div>
<% else %>
<div class="p-4 flex flex-col gap-3 items-center justify-center">
<p class="text-primary font-medium text-sm">No accounts found</p>
<p class="text-secondary text-sm">This connection doesn't have any synchronized accounts yet.</p>
</div>
<% end %>
</div>
<% end %>
</details>
<% end %>

View File

@@ -0,0 +1,38 @@
<% content_for :title, "SimpleFin Connections" %>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-primary">SimpleFin Connections</h1>
<p class="text-secondary mt-1">Manage your SimpleFin bank account connections</p>
</div>
<%= render DS::Link.new(
text: "Add Connection",
icon: "plus",
variant: "primary",
href: new_simplefin_item_path
) %>
</div>
<% if @simplefin_items.any? %>
<div class="space-y-4">
<% @simplefin_items.each do |simplefin_item| %>
<%= render "simplefin_item", simplefin_item: simplefin_item %>
<% end %>
</div>
<% else %>
<div class="text-center py-12">
<div class="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
<%= icon "building-2", size: "lg", class: "text-green-600" %>
</div>
<h3 class="text-lg font-medium text-primary mb-2">No SimpleFin connections</h3>
<p class="text-secondary mb-6">Connect your bank accounts through SimpleFin to automatically sync transactions.</p>
<%= render DS::Link.new(
text: "Add your first connection",
variant: "primary",
href: new_simplefin_item_path
) %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,58 @@
<% content_for :title, "Add SimpleFin Connection" %>
<%= render DS::Dialog.new do |dialog| %>
<% dialog.with_header(title: "Add SimpleFin Connection") do %>
<div class="flex items-center gap-2">
<%= icon "building-2", class: "text-green-600" %>
<span>Connect your bank account through SimpleFin</span>
</div>
<% end %>
<% dialog.with_body do %>
<% if @error_message.present? %>
<%= render DS::Alert.new(message: @error_message, variant: :error) %>
<% end %>
<%= form_with(model: @simplefin_item, local: true, data: { turbo: false }, class: "space-y-6") do |form| %>
<div class="space-y-4">
<div>
<%= form.label :setup_token, "SimpleFin Setup Token", class: "block text-sm font-medium text-primary mb-2" %>
<%= form.text_area :setup_token,
placeholder: "Paste your SimpleFin setup token here...",
rows: 4,
class: "w-full px-3 py-2 border border-primary rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm font-mono" %>
<p class="text-xs text-secondary mt-1">
Get your setup token from
<%= link_to "SimpleFin Bridge", "https://bridge.simplefin.org/simplefin/create",
target: "_blank",
class: "text-blue-600 hover:text-blue-800 underline" %>
</p>
</div>
<div class="bg-blue-50 p-4 rounded-lg">
<div class="flex items-start gap-3">
<%= icon "info", size: "sm", class: "text-blue-600 mt-0.5 flex-shrink-0" %>
<div>
<h3 class="text-sm font-medium text-blue-900 mb-1">How to get your setup token:</h3>
<ol class="text-xs text-blue-800 space-y-1 list-decimal list-inside">
<li>Visit <%= link_to "SimpleFin Bridge", "https://bridge.simplefin.org/simplefin/create", target: "_blank", class: "underline" %></li>
<li>Connect your bank account using your online banking credentials</li>
<li>Copy the SimpleFin setup token that appears (it will be a long Base64-encoded string)</li>
<li>Paste it above and click "Add Connection"</li>
</ol>
<p class="text-xs text-blue-700 mt-2">
<strong>Note:</strong> Setup tokens can only be used once. If the connection fails, you'll need to create a new token.
</p>
</div>
</div>
</div>
</div>
<div class="flex gap-3">
<%= form.submit "Add Connection",
class: "flex-1 bg-blue-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %>
<%= link_to "Cancel",
simplefin_items_path,
class: "px-4 py-2 text-secondary hover:text-primary" %>
</div>
<% end %>
<% end %>
<% end %>

View File

@@ -0,0 +1,69 @@
<% content_for :title, "Set Up SimpleFin Accounts" %>
<%= render DS::Dialog.new do |dialog| %>
<% dialog.with_header(title: "Set Up Your SimpleFin Accounts") do %>
<div class="flex items-center gap-2">
<%= icon "building-2", class: "text-green-600" %>
<span>Choose the correct account types for your imported accounts</span>
</div>
<% end %>
<% dialog.with_body do %>
<%= form_with url: complete_account_setup_simplefin_item_path(@simplefin_item),
method: :post,
local: true,
data: { turbo: false },
class: "space-y-6" do |form| %>
<div class="space-y-4">
<div class="bg-blue-50 p-4 rounded-lg">
<div class="flex items-start gap-3">
<%= icon "info", size: "sm", class: "text-blue-600 mt-0.5 flex-shrink-0" %>
<div>
<p class="text-sm text-blue-900 mb-2">
<strong>Choose the correct account type for each SimpleFin account:</strong>
</p>
<ul class="text-xs text-blue-800 space-y-1 list-disc list-inside">
<li><strong>Checking or Savings</strong> - Regular bank accounts</li>
<li><strong>Credit Card</strong> - Credit card accounts</li>
<li><strong>Investment</strong> - Brokerage, 401(k), IRA accounts</li>
<li><strong>Loan or Mortgage</strong> - Debt accounts</li>
<li><strong>Other Asset</strong> - Everything else</li>
</ul>
</div>
</div>
</div>
<% @simplefin_accounts.each do |simplefin_account| %>
<div class="border border-primary rounded-lg p-4">
<div class="flex items-center justify-between mb-3">
<div>
<h3 class="font-medium text-primary"><%= simplefin_account.name %></h3>
<p class="text-sm text-secondary">
Balance: <%= number_to_currency(simplefin_account.current_balance || 0, unit: simplefin_account.currency) %>
</p>
</div>
</div>
<div>
<%= label_tag "account_types[#{simplefin_account.id}]", "Account Type:",
class: "block text-sm font-medium text-primary mb-2" %>
<%= select_tag "account_types[#{simplefin_account.id}]",
options_for_select(@account_type_options,
Account.map_simplefin_type_to_accountable_type(simplefin_account.account_type, account_name: simplefin_account.name)),
{ class: "w-full px-3 py-2 border border-primary rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" } %>
</div>
</div>
<% end %>
</div>
<div class="flex gap-3">
<%= form.submit "Create Accounts",
class: "flex-1 bg-blue-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %>
<%= link_to "Cancel",
simplefin_items_path,
class: "px-4 py-2 text-secondary hover:text-primary" %>
</div>
<% end %>
<% end %>
<% end %>