diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a78dd2a7..83ea0258 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,18 +85,16 @@ jobs: ruby-version: .ruby-version bundler-cache: true - - name: Run tests + - name: Run tests and smoke test seed env: RAILS_ENV: test DATABASE_URL: postgres://postgres:postgres@localhost:5432 # REDIS_URL: redis://localhost:6379/0 - run: bin/rails db:setup test test:system - - - name: Smoke test database seeds - env: - RAILS_ENV: test - DATABASE_URL: postgres://postgres:postgres@localhost:5432 - run: bin/rails db:drop db:create db:migrate db:seed + run: | + bin/rails db:create + bin/rails db:schema:load + bin/rails test:all + bin/rails db:seed - name: Keep screenshots from failed system tests uses: actions/upload-artifact@v4 diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb new file mode 100644 index 00000000..3a562445 --- /dev/null +++ b/app/controllers/transactions_controller.rb @@ -0,0 +1,64 @@ +class TransactionsController < ApplicationController + before_action :authenticate_user! + before_action :set_transaction, only: %i[ show edit update destroy ] + + def index + @transactions = Current.family.transactions + end + + def show + end + + def new + @transaction = Transaction.new + end + + def edit + end + + def create + @transaction = Transaction.new(transaction_params) + account = Current.family.accounts.find(params[:transaction][:account_id]) + + raise ActiveRecord::RecordNotFound, "Account not found or not accessible" if account.nil? + + @transaction.account = account + + respond_to do |format| + if @transaction.save + format.html { redirect_to transaction_url(@transaction), notice: "Transaction was successfully created." } + else + format.html { render :new, status: :unprocessable_entity } + end + end + end + + def update + respond_to do |format| + if @transaction.update(transaction_params) + format.html { redirect_to transaction_url(@transaction), notice: "Transaction was successfully updated." } + else + format.html { render :edit, status: :unprocessable_entity } + end + end + end + + def destroy + @transaction.destroy! + + respond_to do |format| + format.html { redirect_to transactions_url, notice: "Transaction was successfully destroyed." } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_transaction + @transaction = Transaction.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def transaction_params + params.require(:transaction).permit(:name, :date, :amount, :currency) + end +end diff --git a/app/helpers/application_form_builder.rb b/app/helpers/application_form_builder.rb index 0f560255..e5c1f24a 100644 --- a/app/helpers/application_form_builder.rb +++ b/app/helpers/application_form_builder.rb @@ -34,6 +34,18 @@ class ApplicationFormBuilder < ActionView::Helpers::FormBuilder end end + def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) + default_options = { class: "form-field__input" } + merged_options = default_options.merge(html_options) + + return super(method, collection, value_method, text_method, options, merged_options) unless options[:label] + + @template.form_field_tag do + label(method, *label_args(options)) + + super(method, collection, value_method, text_method, options, merged_options.except(:label)) + end + end + def submit(value = nil, options = {}) value, options = nil, value if value.is_a?(Hash) default_options = { class: "form-field__submit" } diff --git a/app/helpers/transactions_helper.rb b/app/helpers/transactions_helper.rb new file mode 100644 index 00000000..36098d12 --- /dev/null +++ b/app/helpers/transactions_helper.rb @@ -0,0 +1,2 @@ +module TransactionsHelper +end diff --git a/app/javascript/controllers/tabs_controller.js b/app/javascript/controllers/tabs_controller.js new file mode 100644 index 00000000..75902361 --- /dev/null +++ b/app/javascript/controllers/tabs_controller.js @@ -0,0 +1,33 @@ +import { Controller } from "@hotwired/stimulus"; + +// Connects to data-controller="tabs" +export default class extends Controller { + static classes = ["active"]; + static targets = ["btn", "tab"]; + static values = { defaultTab: String }; + + connect() { + const defaultTab = this.defaultTabValue; + this.tabTargets.forEach((tab) => { + if (tab.id === defaultTab) { + tab.hidden = false; + this.btnTargets + .find((btn) => btn.dataset.id === defaultTab) + .classList.add(...this.activeClasses); + } else { + tab.hidden = true; + } + }); + } + + select(event) { + const selectedTabId = event.currentTarget.dataset.id; + this.tabTargets.forEach((tab) => (tab.hidden = tab.id !== selectedTabId)); + this.btnTargets.forEach((btn) => + btn.classList.toggle( + ...this.activeClasses, + btn.dataset.id === selectedTabId + ) + ); + } +} diff --git a/app/models/account.rb b/app/models/account.rb index bfc8dac1..559c1270 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -3,6 +3,7 @@ class Account < ApplicationRecord belongs_to :family has_many :balances, class_name: "AccountBalance" has_many :valuations + has_many :transactions delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy diff --git a/app/models/family.rb b/app/models/family.rb index 38c321c0..fca65cb0 100644 --- a/app/models/family.rb +++ b/app/models/family.rb @@ -1,4 +1,5 @@ class Family < ApplicationRecord has_many :users, dependent: :destroy has_many :accounts, dependent: :destroy + has_many :transactions, through: :accounts end diff --git a/app/models/transaction.rb b/app/models/transaction.rb new file mode 100644 index 00000000..0da6c5db --- /dev/null +++ b/app/models/transaction.rb @@ -0,0 +1,3 @@ +class Transaction < ApplicationRecord + belongs_to :account +end diff --git a/app/views/accounts/_account_history.html.erb b/app/views/accounts/_account_history.html.erb new file mode 100644 index 00000000..25f839c9 --- /dev/null +++ b/app/views/accounts/_account_history.html.erb @@ -0,0 +1,29 @@ +<%# locals: (account:, valuation_series:) %> +
No transactions for this account yet.
+ <% else %> +<%= t('.new_account') %>
<% end %> - <% Accountable.types.each do |type| %> <%= render 'accounts/account_list', type: Accountable.from_type(type) %> <% end %> diff --git a/app/views/transactions/_form.html.erb b/app/views/transactions/_form.html.erb new file mode 100644 index 00000000..9d71cb14 --- /dev/null +++ b/app/views/transactions/_form.html.erb @@ -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 %> diff --git a/app/views/transactions/_transaction.html.erb b/app/views/transactions/_transaction.html.erb new file mode 100644 index 00000000..375cfb80 --- /dev/null +++ b/app/views/transactions/_transaction.html.erb @@ -0,0 +1,12 @@ +<%= transaction.name %>
+<%= transaction.account.name %>
+"><%= number_to_currency(-transaction.amount, { precision: 2 }) %>
+Total transactions
+<%= @transactions.size %>
+Income
++ <%= number_to_currency(@transactions.select { |t| t.amount < 0 }.sum(&:amount).abs, precision: 2) %> +
+Expenses
++ <%= number_to_currency(@transactions.select { |t| t.amount >= 0 }.sum(&:amount), precision: 2) %> +
+transaction
+account
+amount
+No transactions for this account yet.
+ <% else %> +