mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Add transaction modal flow (#633)
* Add transaction modal flow * Preserve decimals when creating transactions
This commit is contained in:
parent
a22c7a0e9c
commit
cd8d741fe1
12 changed files with 137 additions and 22 deletions
|
@ -49,16 +49,20 @@ class TransactionsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@transaction = Transaction.new
|
@transaction = Transaction.new.tap do |txn|
|
||||||
|
if params[:account_id]
|
||||||
|
txn.account = Current.family.accounts.find(params[:account_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
account = Current.family.accounts.find(params[:transaction][:account_id])
|
@transaction = Current.family.accounts
|
||||||
|
.find(params[:transaction][:account_id])
|
||||||
@transaction = account.transactions.build(transaction_params)
|
.transactions.build(transaction_params.merge(amount: amount))
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if @transaction.save
|
if @transaction.save
|
||||||
|
@ -118,6 +122,18 @@ class TransactionsController < ApplicationController
|
||||||
@transaction = Transaction.find(params[:id])
|
@transaction = Transaction.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def amount
|
||||||
|
if nature.income?
|
||||||
|
transaction_params[:amount].to_d * -1
|
||||||
|
else
|
||||||
|
transaction_params[:amount].to_d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def nature
|
||||||
|
params[:transaction][:nature].to_s.inquiry
|
||||||
|
end
|
||||||
|
|
||||||
# Only allow a list of trusted parameters through.
|
# Only allow a list of trusted parameters through.
|
||||||
def transaction_params
|
def transaction_params
|
||||||
params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id)
|
params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id)
|
||||||
|
|
|
@ -2,4 +2,19 @@ module FormsHelper
|
||||||
def form_field_tag(&)
|
def form_field_tag(&)
|
||||||
tag.div class: "form-field", &
|
tag.div class: "form-field", &
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def radio_tab_tag(form:, name:, value:, label:, icon:, checked: false, disabled: false)
|
||||||
|
form.label name, for: form.field_id(name, value), class: "group has-[:disabled]:cursor-not-allowed" do
|
||||||
|
concat radio_tab_contents(label:, icon:)
|
||||||
|
concat form.radio_button(name, value, checked:, disabled:, class: "hidden")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def radio_tab_contents(label:, icon:)
|
||||||
|
tag.div(class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400 group-has-[:checked]:bg-white group-has-[:checked]:text-gray-800 group-has-[:checked]:shadow-sm") do
|
||||||
|
concat lucide_icon(icon, class: "w-5 h-5")
|
||||||
|
concat tag.span(label, class: "group-has-[:checked]:font-semibold")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,6 +17,7 @@ class Account < ApplicationRecord
|
||||||
scope :active, -> { where(is_active: true) }
|
scope :active, -> { where(is_active: true) }
|
||||||
scope :assets, -> { where(classification: "asset") }
|
scope :assets, -> { where(classification: "asset") }
|
||||||
scope :liabilities, -> { where(classification: "liability") }
|
scope :liabilities, -> { where(classification: "liability") }
|
||||||
|
scope :alphabetically, -> { order(:name) }
|
||||||
|
|
||||||
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
|
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ class Transaction::Category < ApplicationRecord
|
||||||
|
|
||||||
before_update :clear_internal_category, if: :name_changed?
|
before_update :clear_internal_category, if: :name_changed?
|
||||||
|
|
||||||
|
scope :alphabetically, -> { order(:name) }
|
||||||
|
|
||||||
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
|
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
|
||||||
|
|
||||||
UNCATEGORIZED_COLOR = "#737373"
|
UNCATEGORIZED_COLOR = "#737373"
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<%# locals: (transactions:)%>
|
<%# locals: (account:, transactions:)%>
|
||||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
<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">
|
<div class="flex justify-between items-center">
|
||||||
<h3 class="font-medium text-lg">Transactions</h3>
|
<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 %>
|
<%= link_to new_transaction_path(account_id: account), class: "flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg", data: { turbo_frame: :modal } do %>
|
||||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
|
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
|
||||||
<span class="text-sm">New transaction</span>
|
<span class="text-sm">New transaction</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
<%= render partial: "accounts/account_history", locals: { account: @account, valuations: @valuation_series } %>
|
<%= render partial: "accounts/account_history", locals: { account: @account, valuations: @valuation_series } %>
|
||||||
</div>
|
</div>
|
||||||
<div data-tabs-target="tab" id="account-transactions-tab" class="hidden">
|
<div data-tabs-target="tab" id="account-transactions-tab" class="hidden">
|
||||||
<%= render partial: "accounts/transactions", locals: { transactions: @account.transactions.order(date: :desc) } %>
|
<%= render partial: "accounts/transactions", locals: { account: @account, transactions: @account.transactions.order(date: :desc) } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<%# locals: (content:) -%>
|
<%# locals: (content:) -%>
|
||||||
<%= turbo_frame_tag "modal" do %>
|
<%= turbo_frame_tag "modal" do %>
|
||||||
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-h-[556px] max-w-[580px] w-full shadow-xs h-full" data-controller="modal" data-action="click->modal#clickOutside">
|
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-h-[648px] max-w-[580px] w-full shadow-xs h-full" data-controller="modal" data-action="click->modal#clickOutside">
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<%= content %>
|
<%= content %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,21 @@
|
||||||
<%= form_with model: @transaction do |f| %>
|
<%= form_with model: @transaction, data: { turbo: false } do |f| %>
|
||||||
<%= f.collection_select :account_id, Current.family.accounts, :id, :name, { prompt: "Select an Account", label: "Account" } %>
|
<section>
|
||||||
<%= f.date_field :date, label: "Date" %>
|
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2">
|
||||||
<%= f.text_field :name, label: "Name", placeholder: "Groceries" %>
|
<%= radio_tab_tag form: f, name: :nature, value: :expense, label: t(".expense"), icon: "minus-circle", checked: true %>
|
||||||
<%= f.money_field :amount_money, label: "Amount" %>
|
<%= radio_tab_tag form: f, name: :nature, value: :income, label: t(".income"), icon: "plus-circle" %>
|
||||||
<%= f.submit %>
|
<%= radio_tab_tag form: f, name: :nature, value: :transfer, label: t(".transfer"), icon: "arrow-right-left", disabled: true %>
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="space-y-2">
|
||||||
|
<%= 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 %>
|
||||||
|
<%= f.money_field :amount_money, label: t(".amount"), required: true %>
|
||||||
|
<%= f.collection_select :category_id, Current.family.transaction_categories.alphabetically, :id, :name, { prompt: t(".category_prompt"), label: t(".category") }, required: true %>
|
||||||
|
<%= f.date_field :date, label: t(".date"), required: true %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<%= f.submit t(".submit") %>
|
||||||
|
</section>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<h1 class="text-xl">Transactions</h1>
|
<h1 class="text-xl">Transactions</h1>
|
||||||
<div class="flex items-center gap-5">
|
<div class="flex items-center gap-5">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<%= link_to new_transaction_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2" do %>
|
<%= link_to new_transaction_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
|
||||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||||
<p>New transaction</p>
|
<p>New transaction</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<div class="mx-auto md:w-2/3 w-full">
|
<%= modal do %>
|
||||||
<h1 class="font-bold text-4xl mb-4">New transaction</h1>
|
<article class="mx-auto w-full p-4 space-y-4">
|
||||||
<%= render "form", transaction: @transaction %>
|
<header class="flex justify-between">
|
||||||
</div>
|
<h2 class="font-medium text-xl">New transaction</h2>
|
||||||
<div class="flex justify-center">
|
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||||
<%= link_to "Back to transactions", transactions_path, class: "mt-8 underline text-lg font-bold" %>
|
</header>
|
||||||
</div>
|
|
||||||
|
<%= render "form", transaction: @transaction %>
|
||||||
|
</article>
|
||||||
|
<% end %>
|
||||||
|
|
|
@ -14,5 +14,18 @@ en:
|
||||||
success: New transaction created successfully
|
success: New transaction created successfully
|
||||||
destroy:
|
destroy:
|
||||||
success: Transaction deleted successfully
|
success: Transaction deleted successfully
|
||||||
|
form:
|
||||||
|
account: Account
|
||||||
|
account_prompt: Select an Account
|
||||||
|
amount: Amount
|
||||||
|
category: Category
|
||||||
|
category_prompt: Select a Category
|
||||||
|
date: Date
|
||||||
|
description: Description
|
||||||
|
description_placeholder: Describe transaction
|
||||||
|
expense: Expense
|
||||||
|
income: Income
|
||||||
|
submit: Add transaction
|
||||||
|
transfer: Transfer
|
||||||
update:
|
update:
|
||||||
success: Transaction updated successfully
|
success: Transaction updated successfully
|
||||||
|
|
|
@ -16,6 +16,12 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_response :success
|
assert_response :success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "prefills account_id if provided" do
|
||||||
|
get new_transaction_url(account_id: @transaction.account_id)
|
||||||
|
assert_response :success
|
||||||
|
assert_select "option[selected][value='#{@transaction.account_id}']"
|
||||||
|
end
|
||||||
|
|
||||||
test "should create transaction" do
|
test "should create transaction" do
|
||||||
name = "transaction_name"
|
name = "transaction_name"
|
||||||
assert_difference("Transaction.count") do
|
assert_difference("Transaction.count") do
|
||||||
|
@ -25,6 +31,51 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_redirected_to transactions_url
|
assert_redirected_to transactions_url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "creation preserves decimals" do
|
||||||
|
assert_difference("Transaction.count") do
|
||||||
|
post transactions_url, params: { transaction: {
|
||||||
|
nature: "expense",
|
||||||
|
account_id: @transaction.account_id,
|
||||||
|
amount: 123.45,
|
||||||
|
currency: @transaction.currency,
|
||||||
|
date: @transaction.date,
|
||||||
|
name: @transaction.name } }
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to transactions_url
|
||||||
|
assert_equal 123.45.to_d, Transaction.order(created_at: :desc).first.amount
|
||||||
|
end
|
||||||
|
|
||||||
|
test "expenses are positive" do
|
||||||
|
assert_difference("Transaction.count") do
|
||||||
|
post transactions_url, params: { transaction: {
|
||||||
|
nature: "expense",
|
||||||
|
account_id: @transaction.account_id,
|
||||||
|
amount: @transaction.amount,
|
||||||
|
currency: @transaction.currency,
|
||||||
|
date: @transaction.date,
|
||||||
|
name: @transaction.name } }
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to transactions_url
|
||||||
|
assert Transaction.order(created_at: :desc).first.amount.positive?, "Amount should be positive"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "incomes are negative" do
|
||||||
|
assert_difference("Transaction.count") do
|
||||||
|
post transactions_url, params: { transaction: {
|
||||||
|
nature: "income",
|
||||||
|
account_id: @transaction.account_id,
|
||||||
|
amount: @transaction.amount,
|
||||||
|
currency: @transaction.currency,
|
||||||
|
date: @transaction.date,
|
||||||
|
name: @transaction.name } }
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to transactions_url
|
||||||
|
assert Transaction.order(created_at: :desc).first.amount.negative?, "Amount should be negative"
|
||||||
|
end
|
||||||
|
|
||||||
test "should show transaction" do
|
test "should show transaction" do
|
||||||
get transaction_url(@transaction)
|
get transaction_url(@transaction)
|
||||||
assert_response :success
|
assert_response :success
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue