1
0
Fork 0
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:
Jose Farias 2024-04-16 12:44:31 -06:00 committed by GitHub
parent a22c7a0e9c
commit cd8d741fe1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 137 additions and 22 deletions

View file

@ -49,16 +49,20 @@ class TransactionsController < ApplicationController
end
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
def edit
end
def create
account = Current.family.accounts.find(params[:transaction][:account_id])
@transaction = account.transactions.build(transaction_params)
@transaction = Current.family.accounts
.find(params[:transaction][:account_id])
.transactions.build(transaction_params.merge(amount: amount))
respond_to do |format|
if @transaction.save
@ -118,6 +122,18 @@ class TransactionsController < ApplicationController
@transaction = Transaction.find(params[:id])
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.
def transaction_params
params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id)

View file

@ -2,4 +2,19 @@ module FormsHelper
def form_field_tag(&)
tag.div class: "form-field", &
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

View file

@ -17,6 +17,7 @@ class Account < ApplicationRecord
scope :active, -> { where(is_active: true) }
scope :assets, -> { where(classification: "asset") }
scope :liabilities, -> { where(classification: "liability") }
scope :alphabetically, -> { order(:name) }
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy

View file

@ -6,6 +6,8 @@ class Transaction::Category < ApplicationRecord
before_update :clear_internal_category, if: :name_changed?
scope :alphabetically, -> { order(:name) }
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
UNCATEGORIZED_COLOR = "#737373"

View file

@ -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="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 %>
<%= 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") %>
<span class="text-sm">New transaction</span>
<% end %>

View file

@ -71,7 +71,7 @@
<%= render partial: "accounts/account_history", locals: { account: @account, valuations: @valuation_series } %>
</div>
<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>

View file

@ -1,6 +1,6 @@
<%# locals: (content:) -%>
<%= 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">
<%= content %>
</div>

View file

@ -1,7 +1,21 @@
<%= 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", placeholder: "Groceries" %>
<%= f.money_field :amount_money, label: "Amount" %>
<%= f.submit %>
<%= form_with model: @transaction, data: { turbo: false } do |f| %>
<section>
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2">
<%= radio_tab_tag form: f, name: :nature, value: :expense, label: t(".expense"), icon: "minus-circle", checked: true %>
<%= radio_tab_tag form: f, name: :nature, value: :income, label: t(".income"), icon: "plus-circle" %>
<%= 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 %>

View file

@ -3,7 +3,7 @@
<h1 class="text-xl">Transactions</h1>
<div class="flex items-center gap-5">
<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") %>
<p>New transaction</p>
<% end %>

View file

@ -1,7 +1,10 @@
<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>
<%= modal do %>
<article class="mx-auto w-full p-4 space-y-4">
<header class="flex justify-between">
<h2 class="font-medium text-xl">New transaction</h2>
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
</header>
<%= render "form", transaction: @transaction %>
</article>
<% end %>

View file

@ -14,5 +14,18 @@ en:
success: New transaction created successfully
destroy:
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:
success: Transaction updated successfully

View file

@ -16,6 +16,12 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
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
name = "transaction_name"
assert_difference("Transaction.count") do
@ -25,6 +31,51 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to transactions_url
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
get transaction_url(@transaction)
assert_response :success