mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-05 05:25:24 +02:00
Scaffold out basic transactions model and UI (#478)
* Transaction scaffold * Rough in transaction views * Fix sort order * Fix mass assignment issue * Fix test * Simplify CI workflow * Don't seed db before test
This commit is contained in:
parent
e767aca37f
commit
87b97b3c41
26 changed files with 430 additions and 44 deletions
29
app/views/accounts/_account_history.html.erb
Normal file
29
app/views/accounts/_account_history.html.erb
Normal file
|
@ -0,0 +1,29 @@
|
|||
<%# locals: (account:, valuation_series:) %>
|
||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="font-medium text-lg">History</h3>
|
||||
<%= 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") %>
|
||||
<span class="text-sm">New entry</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="rounded-xl bg-gray-25 p-1">
|
||||
<div class="flex flex-col rounded-lg space-y-1">
|
||||
<div class="text-xs font-medium text-gray-500 uppercase flex items-center px-4 py-2">
|
||||
<div class="w-16">date</div>
|
||||
<div class="flex items-center justify-between grow">
|
||||
<div></div>
|
||||
<div>value</div>
|
||||
</div>
|
||||
<div class="w-56 text-right">change</div>
|
||||
<div class="w-[72px]"></div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
|
||||
<%= turbo_frame_tag dom_id(Valuation.new) %>
|
||||
<%= turbo_frame_tag "valuations_list" do %>
|
||||
<%= render partial: "accounts/account_valuation_list", locals: { valuation_series: valuation_series } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
19
app/views/accounts/_transactions.html.erb
Normal file
19
app/views/accounts/_transactions.html.erb
Normal file
|
@ -0,0 +1,19 @@
|
|||
<%# locals: (transactions:)%>
|
||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="font-medium text-lg">Transactions</h3>
|
||||
<%= link_to new_transaction_path, 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") %>
|
||||
<span class="text-sm">New transaction</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if transactions.empty? %>
|
||||
<p class="text-gray-500 py-4">No transactions for this account yet.</p>
|
||||
<% else %>
|
||||
<div class="space-y-6">
|
||||
<% transactions.group_by { |transaction| transaction.date }.each do |date, grouped_transactions| %>
|
||||
<%= render partial: "transactions/transaction_group", locals: { date: date, transactions: grouped_transactions } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
|
@ -59,31 +59,17 @@
|
|||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="font-medium text-lg">History</h3>
|
||||
<%= 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") %>
|
||||
<span class="text-sm">New entry</span>
|
||||
<% end %>
|
||||
<div data-controller="tabs" data-tabs-active-class="bg-gray-100" data-tabs-default-tab-value="account-history-tab">
|
||||
<div class="flex gap-1 text-sm text-gray-900 font-medium mb-4">
|
||||
<button data-id="account-history-tab" class="p-2 rounded-lg" data-tabs-target="btn" data-action="click->tabs#select">History</button>
|
||||
<button data-id="account-transactions-tab" class="p-2 rounded-lg" data-tabs-target="btn" data-action="click->tabs#select">Transactions</button>
|
||||
</div>
|
||||
<div class="rounded-xl bg-gray-25 p-1">
|
||||
<div class="flex flex-col rounded-lg space-y-1">
|
||||
<div class="text-xs font-medium text-gray-500 uppercase flex items-center px-4 py-2">
|
||||
<div class="w-16">date</div>
|
||||
<div class="flex items-center justify-between grow">
|
||||
<div></div>
|
||||
<div>value</div>
|
||||
</div>
|
||||
<div class="w-56 text-right">change</div>
|
||||
<div class="w-[72px]"></div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
|
||||
<%= turbo_frame_tag dom_id(Valuation.new) %>
|
||||
<%= turbo_frame_tag "valuations_list" do %>
|
||||
<%= render partial: "accounts/account_valuation_list", locals: { valuation_series: @valuation_series } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="min-h-[800px]">
|
||||
<div data-tabs-target="tab" id="account-history-tab">
|
||||
<%= render partial: "accounts/account_history", locals: { account: @account, valuation_series: @valuation_series } %>
|
||||
</div>
|
||||
<div data-tabs-target="tab" id="account-transactions-tab">
|
||||
<%= render partial: "accounts/transactions", locals: { transactions: @account.transactions.order(date: :desc) } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,10 +6,8 @@
|
|||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Maybe">
|
||||
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="icon" href="/icon.png" type="image/png">
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||
|
@ -18,15 +16,12 @@
|
|||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
<%= javascript_importmap_tags %>
|
||||
<%= hotwire_livereload_tags if Rails.env.development? %>
|
||||
|
||||
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
|
||||
<%= yield :head %>
|
||||
</head>
|
||||
|
||||
<body class="h-full">
|
||||
<div id="notification-tray" class="fixed top-6 right-6 space-y-1 z-50"></div>
|
||||
<%= safe_join(flash.map { |type, message| notification(message, type: type) }) %>
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex-col p-5 min-w-80">
|
||||
<div class="flex items-center justify-between">
|
||||
|
@ -37,14 +32,12 @@
|
|||
<div class="flex" data-action="click->dropdown#toggleMenu">
|
||||
<div class="mr-1.5 text-white w-8 h-8 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.email.first %></div>
|
||||
</div>
|
||||
|
||||
<div class="absolute z-10 hidden w-screen px-2 mt-2 -translate-x-1/2 left-1/2 max-w-min" data-dropdown-target="menu">
|
||||
<div class="w-48 px-3 text-sm font-semibold leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= link_to "Settings", edit_settings_path, class: 'block p-2 hover:text-gray-600' %>
|
||||
<%= button_to "Log Out", session_path, method: :delete, class: 'block p-2 hover:text-gray-600' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
|
@ -56,7 +49,7 @@
|
|||
<%= sidebar_link_to t('.accounts'), accounts_path, icon: 'layers' %>
|
||||
</li>
|
||||
<li>
|
||||
<%= sidebar_link_to t('.transactions'), "#", icon: 'credit-card' %>
|
||||
<%= sidebar_link_to t('.transactions'), transactions_path, icon: 'credit-card' %>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
@ -69,13 +62,10 @@
|
|||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
||||
<%= link_to new_account_path, 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 %>
|
||||
<%= 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) %>
|
||||
<% end %>
|
||||
|
|
7
app/views/transactions/_form.html.erb
Normal file
7
app/views/transactions/_form.html.erb
Normal file
|
@ -0,0 +1,7 @@
|
|||
<%= form_with model: @transaction do |f| %>
|
||||
<%= f.collection_select :account_id, Current.family.accounts, :id, :name, { prompt: "Select an Account", label: "Account" } %>
|
||||
<%= f.date_field :date, label: "Date" %>
|
||||
<%= f.text_field :name, label: "Name" %>
|
||||
<%= f.number_field :amount, label: "Amount", step: :any, placeholder: number_to_currency(0), in: 0.00..100000000.00 %>
|
||||
<%= f.submit %>
|
||||
<% end %>
|
12
app/views/transactions/_transaction.html.erb
Normal file
12
app/views/transactions/_transaction.html.erb
Normal file
|
@ -0,0 +1,12 @@
|
|||
<div id="<%= dom_id transaction %>" class="flex items-center gap-6 py-4 text-sm font-medium">
|
||||
<div class="w-96 flex items-center gap-2">
|
||||
<div class="w-8 h-8 flex items-center justify-center rounded-full bg-gray-600/5 text-gray-600"><%= transaction.name[0].upcase %></div>
|
||||
<p class="text-gray-900"><%= transaction.name %></p>
|
||||
</div>
|
||||
<div>
|
||||
<p><%= transaction.account.name %></p>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<p class="<%= transaction.amount < 0 ? "text-green-600" : "" %>"><%= number_to_currency(-transaction.amount, { precision: 2 }) %></p>
|
||||
</div>
|
||||
</div>
|
10
app/views/transactions/_transaction_group.html.erb
Normal file
10
app/views/transactions/_transaction_group.html.erb
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%# locals: (date:, transactions:) %>
|
||||
<div class="bg-gray-25 rounded-xl p-1">
|
||||
<div class="py-2 px-4 flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
<h4><%= date.strftime('%b %d, %Y') %> · <%= transactions.size %></h4>
|
||||
<span><%= number_to_currency(-transactions.sum(&:amount)) %></span>
|
||||
</div>
|
||||
<div class="bg-white shadow-xs rounded-md border border-alpha-black-25 divide-y divide-alpha-black-50 px-4">
|
||||
<%= render partial: "transactions/transaction", collection: transactions %>
|
||||
</div>
|
||||
</div>
|
8
app/views/transactions/edit.html.erb
Normal file
8
app/views/transactions/edit.html.erb
Normal file
|
@ -0,0 +1,8 @@
|
|||
<div class="mx-auto md:w-2/3 w-full">
|
||||
<h1 class="font-bold text-4xl">Editing transaction</h1>
|
||||
|
||||
<%= render "form", transaction: @transaction %>
|
||||
|
||||
<%= link_to "Show this transaction", @transaction, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
<%= link_to "Back to transactions", transactions_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
</div>
|
75
app/views/transactions/index.html.erb
Normal file
75
app/views/transactions/index.html.erb
Normal file
|
@ -0,0 +1,75 @@
|
|||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center text-gray-900 font-medium">
|
||||
<h1 class="text-xl">Transactions</h1>
|
||||
<div class="flex items-center gap-5">
|
||||
<div class="flex items-center gap-1 cursor-not-allowed">
|
||||
<span class="text-sm">USD $</span>
|
||||
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
|
||||
</div>
|
||||
<div class="border-r border-alpha-black-200 h-5"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= lucide_icon("settings-2", class: "cursor-not-allowed w-5 h-5 text-gray-500") %>
|
||||
<%= link_to new_transaction_path, class: "rounded-full w-9 h-9 bg-gray-900 text-white flex items-center justify-center hover:bg-gray-700" do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 bg-white rounded-xl border border-alpha-black-25 shadow-xs px-4 divide-x divide-alpha-black-100">
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-gray-500">Total transactions</p>
|
||||
<p class="text-gray-900 font-medium text-xl"><%= @transactions.size %></p>
|
||||
</div>
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-gray-500">Income</p>
|
||||
<p class="text-gray-900 font-medium text-xl">
|
||||
<%= number_to_currency(@transactions.select { |t| t.amount < 0 }.sum(&:amount).abs, precision: 2) %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-gray-500">Expenses</p>
|
||||
<p class="text-gray-900 font-medium text-xl">
|
||||
<%= number_to_currency(@transactions.select { |t| t.amount >= 0 }.sum(&:amount), precision: 2) %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="transactions" class="bg-white rounded-xl border border-alpha-black-25 shadow-xs p-4 space-y-4">
|
||||
<div class="flex gap-2">
|
||||
<div class="grow cursor-not-allowed">
|
||||
<%= form_with url: transactions_path, method: :get, local: true, html: { role: 'search' } do |form| %>
|
||||
<div class="relative flex items-center bg-white border border-gray-200 rounded-lg">
|
||||
<%= form.text_field :search, placeholder: "Search transaction by merchant, category or amount", class: "placeholder:text-sm placeholder:text-gray-500 relative pl-10 w-full border-none rounded-lg cursor-not-allowed", 'data-action': "input->search#perform", disabled: true %>
|
||||
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500 ml-2 absolute inset-0 transform top-1/2 -translate-y-1/2") %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<button class="cursor-not-allowed border border-gray-200 block h-full rounded-lg flex items-center gap-2 px-4">
|
||||
<%= lucide_icon("list-filter", class: "w-5 h-5 text-gray-500") %>
|
||||
<p class="text-sm font-medium text-gray-900">Filter</p>
|
||||
</button>
|
||||
</div>
|
||||
<%= form_with url: "#", method: :get, class: "flex items-center gap-4", html: { class: "" } do |f| %>
|
||||
<%= f.select :period, options_for_select([['7D', 'last_7_days'], ['1M', 'last_30_days'], ["1Y", "last_365_days"], ['All', 'all']], selected: params[:period]), {}, { class: "block h-full w-full border border-gray-200 rounded-lg text-sm py-2 pr-8 pl-2 cursor-not-allowed", onchange: "this.form.submit();", disabled: true } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="bg-gray-25 rounded-xl px-5 py-3 text-xs font-medium text-gray-500 flex items-center gap-6">
|
||||
<div class="w-96">
|
||||
<p class="uppercase">transaction</p>
|
||||
</div>
|
||||
<div class="grow uppercase flex justify-between items-center gap-5 text-xs font-medium text-gray-500">
|
||||
<p>account</p>
|
||||
<p>amount</p>
|
||||
</div>
|
||||
</div>
|
||||
<% if @transactions.empty? %>
|
||||
<p class="text-gray-500 py-4">No transactions for this account yet.</p>
|
||||
<% else %>
|
||||
<div class="space-y-6">
|
||||
<% @transactions.group_by { |transaction| transaction.date }.each do |date, grouped_transactions| %>
|
||||
<%= render partial: "transactions/transaction_group", locals: { date: date, transactions: grouped_transactions } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
7
app/views/transactions/new.html.erb
Normal file
7
app/views/transactions/new.html.erb
Normal file
|
@ -0,0 +1,7 @@
|
|||
<div class="mx-auto md:w-2/3 w-full">
|
||||
<h1 class="font-bold text-4xl mb-4">New transaction</h1>
|
||||
<%= render "form", transaction: @transaction %>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<%= link_to "Back to transactions", transactions_path, class: "mt-8 underline text-lg font-bold" %>
|
||||
</div>
|
10
app/views/transactions/show.html.erb
Normal file
10
app/views/transactions/show.html.erb
Normal file
|
@ -0,0 +1,10 @@
|
|||
<div class="mx-auto md:w-2/3 w-full flex">
|
||||
<div class="mx-auto">
|
||||
<%= render @transaction %>
|
||||
<%= link_to "Edit this transaction", edit_transaction_path(@transaction), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
<div class="inline-block ml-2">
|
||||
<%= button_to "Destroy this transaction", transaction_path(@transaction), method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %>
|
||||
</div>
|
||||
<%= link_to "Back to transactions", transactions_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
</div>
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue