diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index 5e7322ad..36e45b01 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -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) diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb index ac138976..ea7f24db 100644 --- a/app/helpers/forms_helper.rb +++ b/app/helpers/forms_helper.rb @@ -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 diff --git a/app/models/account.rb b/app/models/account.rb index 550e0279..b1d6aec1 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -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 diff --git a/app/models/transaction/category.rb b/app/models/transaction/category.rb index 614ec82b..97439f68 100644 --- a/app/models/transaction/category.rb +++ b/app/models/transaction/category.rb @@ -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" diff --git a/app/views/accounts/_transactions.html.erb b/app/views/accounts/_transactions.html.erb index cc050643..ad040ff7 100644 --- a/app/views/accounts/_transactions.html.erb +++ b/app/views/accounts/_transactions.html.erb @@ -1,8 +1,8 @@ -<%# locals: (transactions:)%> +<%# locals: (account:, transactions:)%>

Transactions

- <%= 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") %> New transaction <% end %> diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index 48be2c56..7ef90757 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -71,7 +71,7 @@ <%= render partial: "accounts/account_history", locals: { account: @account, valuations: @valuation_series } %>
diff --git a/app/views/shared/_modal.html.erb b/app/views/shared/_modal.html.erb index 511991e0..75174faf 100644 --- a/app/views/shared/_modal.html.erb +++ b/app/views/shared/_modal.html.erb @@ -1,6 +1,6 @@ <%# locals: (content:) -%> <%= turbo_frame_tag "modal" do %> - +
<%= content %>
diff --git a/app/views/transactions/_form.html.erb b/app/views/transactions/_form.html.erb index 7799f8c2..2db4d837 100644 --- a/app/views/transactions/_form.html.erb +++ b/app/views/transactions/_form.html.erb @@ -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| %> +
+
+ <%= 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 %> +
+
+ +
+ <%= 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 %> +
+ +
+ <%= f.submit t(".submit") %> +
<% end %> diff --git a/app/views/transactions/index.html.erb b/app/views/transactions/index.html.erb index ee244218..829b6575 100644 --- a/app/views/transactions/index.html.erb +++ b/app/views/transactions/index.html.erb @@ -3,7 +3,7 @@

Transactions

- <%= 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") %>

New transaction

<% end %> diff --git a/app/views/transactions/new.html.erb b/app/views/transactions/new.html.erb index 03da6bb2..9ad41294 100644 --- a/app/views/transactions/new.html.erb +++ b/app/views/transactions/new.html.erb @@ -1,7 +1,10 @@ -
-

New transaction

- <%= render "form", transaction: @transaction %> -
-
- <%= link_to "Back to transactions", transactions_path, class: "mt-8 underline text-lg font-bold" %> -
+<%= modal do %> +
+
+

New transaction

+ <%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %> +
+ + <%= render "form", transaction: @transaction %> +
+<% end %> diff --git a/config/locales/views/transaction/en.yml b/config/locales/views/transaction/en.yml index 8e31011e..740d27cb 100644 --- a/config/locales/views/transaction/en.yml +++ b/config/locales/views/transaction/en.yml @@ -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 diff --git a/test/controllers/transactions_controller_test.rb b/test/controllers/transactions_controller_test.rb index 168523e5..c57f1403 100644 --- a/test/controllers/transactions_controller_test.rb +++ b/test/controllers/transactions_controller_test.rb @@ -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