diff --git a/app/controllers/account/cashes_controller.rb b/app/controllers/account/cashes_controller.rb
index 6afa3241..f94582ce 100644
--- a/app/controllers/account/cashes_controller.rb
+++ b/app/controllers/account/cashes_controller.rb
@@ -1,14 +1,7 @@
class Account::CashesController < ApplicationController
layout :with_sidebar
- before_action :set_account
-
def index
+ @account = Current.family.accounts.find(params[:account_id])
end
-
- private
-
- def set_account
- @account = Current.family.accounts.find(params[:account_id])
- end
end
diff --git a/app/controllers/account/entries_controller.rb b/app/controllers/account/entries_controller.rb
index d78cb62c..b36cdbc6 100644
--- a/app/controllers/account/entries_controller.rb
+++ b/app/controllers/account/entries_controller.rb
@@ -2,56 +2,21 @@ class Account::EntriesController < ApplicationController
layout :with_sidebar
before_action :set_account
- before_action :set_entry, only: %i[edit update show destroy]
def index
@q = search_params
- @pagy, @entries = pagy(@account.entries.search(@q).reverse_chronological, limit: params[:per_page] || "10")
- end
-
- def edit
- render entryable_view_path(:edit)
- end
-
- def update
- prev_amount = @entry.amount
- prev_date = @entry.date
-
- @entry.update!(entry_params)
- @entry.sync_account_later if prev_amount != @entry.amount || prev_date != @entry.date
-
- respond_to do |format|
- format.html { redirect_to account_entry_path(@account, @entry), notice: t(".success") }
- format.turbo_stream { render turbo_stream: turbo_stream.replace(@entry) }
- end
- end
-
- def show
- render entryable_view_path(:show)
- end
-
- def destroy
- @entry.destroy!
- @entry.sync_account_later
- redirect_to account_url(@entry.account), notice: t(".success")
+ @pagy, @entries = pagy(entries_scope.search(@q).reverse_chronological, limit: params[:per_page] || "10")
end
private
-
- def entryable_view_path(action)
- @entry.entryable_type.underscore.pluralize + "/" + action.to_s
- end
-
def set_account
@account = Current.family.accounts.find(params[:account_id])
end
- def set_entry
- @entry = @account.entries.find(params[:id])
- end
-
- def entry_params
- params.require(:account_entry).permit(:name, :date, :amount, :currency, :notes)
+ def entries_scope
+ scope = Current.family.entries
+ scope = scope.where(account: @account) if @account
+ scope
end
def search_params
diff --git a/app/controllers/account/holdings_controller.rb b/app/controllers/account/holdings_controller.rb
index af0d3e6a..c316b854 100644
--- a/app/controllers/account/holdings_controller.rb
+++ b/app/controllers/account/holdings_controller.rb
@@ -1,11 +1,12 @@
class Account::HoldingsController < ApplicationController
layout :with_sidebar
- before_action :set_account
before_action :set_holding, only: %i[show destroy]
def index
- @holdings = @account.holdings.current
+ @account = Current.family.accounts.find(params[:account_id])
+ @holdings = Current.family.holdings.current
+ @holdings = @holdings.where(account: @account) if @account
end
def show
@@ -13,16 +14,17 @@ class Account::HoldingsController < ApplicationController
def destroy
@holding.destroy_holding_and_entries!
- redirect_back_or_to account_holdings_path(@account)
+
+ flash[:notice] = t(".success")
+
+ respond_to do |format|
+ format.html { redirect_back_or_to account_path(@holding.account) }
+ format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, account_path(@holding.account)) }
+ end
end
private
-
- def set_account
- @account = Current.family.accounts.find(params[:account_id])
- end
-
def set_holding
- @holding = @account.holdings.current.find(params[:id])
+ @holding = Current.family.holdings.current.find(params[:id])
end
end
diff --git a/app/controllers/account/trades_controller.rb b/app/controllers/account/trades_controller.rb
index f57c3089..6ace6538 100644
--- a/app/controllers/account/trades_controller.rb
+++ b/app/controllers/account/trades_controller.rb
@@ -1,69 +1,37 @@
class Account::TradesController < ApplicationController
- layout :with_sidebar
+ include EntryableResource
- before_action :set_account
- before_action :set_entry, only: :update
-
- def new
- @entry = @account.entries.account_trades.new(
- currency: @account.currency,
- entryable_attributes: {}
- )
- end
-
- def index
- @entries = @account.entries.reverse_chronological.where(entryable_type: %w[Account::Trade Account::Transaction])
- end
-
- def create
- @builder = Account::EntryBuilder.new(entry_params)
-
- if entry = @builder.save
- entry.sync_account_later
- redirect_to @account, notice: t(".success")
- else
- flash[:alert] = t(".failure")
- redirect_back_or_to @account
- end
- end
-
- def update
- @entry.update!(entry_params)
-
- respond_to do |format|
- format.html { redirect_to account_entry_path(@account, @entry), notice: t(".success") }
- format.turbo_stream { render turbo_stream: turbo_stream.replace(@entry) }
- end
- end
-
- def securities
- query = params[:q]
- return render json: [] if query.blank? || query.length < 2 || query.length > 100
-
- @securities = Security::SynthComboboxOption.find_in_synth(query)
- end
+ permitted_entryable_attributes :id, :qty, :price
private
-
- def set_account
- @account = Current.family.accounts.find(params[:account_id])
+ def build_entry
+ Account::TradeBuilder.new(create_entry_params)
end
- def set_entry
- @entry = @account.entries.find(params[:id])
+ def create_entry_params
+ params.require(:account_entry).permit(
+ :account_id, :date, :amount, :currency, :qty, :price, :ticker, :type, :transfer_account_id
+ ).tap do |params|
+ account_id = params.delete(:account_id)
+ params[:account] = Current.family.accounts.find(account_id)
+ end
end
- def entry_params
- params.require(:account_entry)
- .permit(
- :type, :date, :qty, :ticker, :price, :amount, :notes, :excluded, :currency, :transfer_account_id, :entryable_type,
- entryable_attributes: [
- :id,
- :qty,
- :ticker,
- :price
- ]
- )
- .merge(account: @account)
+ def update_entry_params
+ return entry_params unless entry_params[:entryable_attributes].present?
+
+ update_params = entry_params
+ update_params = update_params.merge(entryable_type: "Account::Trade")
+
+ qty = update_params[:entryable_attributes][:qty]
+ price = update_params[:entryable_attributes][:price]
+
+ if qty.present? && price.present?
+ qty = update_params[:nature] == "inflow" ? -qty.to_d : qty.to_d
+ update_params[:entryable_attributes][:qty] = qty
+ update_params[:amount] = qty * price.to_d
+ end
+
+ update_params.except(:nature)
end
end
diff --git a/app/controllers/account/transaction_categories_controller.rb b/app/controllers/account/transaction_categories_controller.rb
new file mode 100644
index 00000000..5920a0b3
--- /dev/null
+++ b/app/controllers/account/transaction_categories_controller.rb
@@ -0,0 +1,22 @@
+class Account::TransactionCategoriesController < ApplicationController
+ def update
+ @entry = Current.family.entries.account_transactions.find(params[:transaction_id])
+ @entry.update!(entry_params)
+
+ respond_to do |format|
+ format.html { redirect_back_or_to account_transaction_path(@entry) }
+ format.turbo_stream do
+ render turbo_stream: turbo_stream.replace(
+ "category_menu_account_transaction_#{@entry.account_transaction_id}",
+ partial: "categories/menu",
+ locals: { transaction: @entry.account_transaction }
+ )
+ end
+ end
+ end
+
+ private
+ def entry_params
+ params.require(:account_entry).permit(:entryable_type, entryable_attributes: [ :id, :category_id ])
+ end
+end
diff --git a/app/controllers/account/transactions_controller.rb b/app/controllers/account/transactions_controller.rb
index 26c9e62d..6784aac6 100644
--- a/app/controllers/account/transactions_controller.rb
+++ b/app/controllers/account/transactions_controller.rb
@@ -1,74 +1,55 @@
class Account::TransactionsController < ApplicationController
- layout :with_sidebar
+ include EntryableResource
- before_action :set_account
- before_action :set_entry, only: :update
+ permitted_entryable_attributes :id, :category_id, :merchant_id, { tag_ids: [] }
- def index
- @pagy, @entries = pagy(
- @account.entries.account_transactions.reverse_chronological,
- limit: params[:per_page] || "10"
- )
+ def bulk_delete
+ destroyed = Current.family.entries.destroy_by(id: bulk_delete_params[:entry_ids])
+ destroyed.map(&:account).uniq.each(&:sync_later)
+ redirect_back_or_to transactions_url, notice: t(".success", count: destroyed.count)
end
- def update
- prev_amount = @entry.amount
- prev_date = @entry.date
+ def bulk_edit
+ end
- @entry.update!(entry_params.except(:origin))
- @entry.sync_account_later if prev_amount != @entry.amount || prev_date != @entry.date
+ def bulk_update
+ updated = Current.family
+ .entries
+ .where(id: bulk_update_params[:entry_ids])
+ .bulk_update!(bulk_update_params)
- respond_to do |format|
- format.html { redirect_to account_entry_path(@account, @entry), notice: t(".success") }
- format.turbo_stream do
- render turbo_stream: turbo_stream.replace(
- @entry,
- partial: "account/entries/entry",
- locals: entry_locals.merge(entry: @entry)
- )
- end
- end
+ redirect_back_or_to transactions_url, notice: t(".success", count: updated)
+ end
+
+ def mark_transfers
+ Current.family
+ .entries
+ .where(id: bulk_update_params[:entry_ids])
+ .mark_transfers!
+
+ redirect_back_or_to transactions_url, notice: t(".success")
+ end
+
+ def unmark_transfers
+ Current.family
+ .entries
+ .where(id: bulk_update_params[:entry_ids])
+ .update_all marked_as_transfer: false
+
+ redirect_back_or_to transactions_url, notice: t(".success")
end
private
- def set_account
- @account = Current.family.accounts.find(params[:account_id])
+ def bulk_delete_params
+ params.require(:bulk_delete).permit(entry_ids: [])
end
- def set_entry
- @entry = @account.entries.find(params[:id])
+ def bulk_update_params
+ params.require(:bulk_update).permit(:date, :notes, :category_id, :merchant_id, entry_ids: [])
end
- def entry_locals
- {
- selectable: entry_params[:origin].present?,
- show_balance: entry_params[:origin] == "account",
- origin: entry_params[:origin]
- }
- end
-
- def entry_params
- params.require(:account_entry)
- .permit(
- :name, :date, :amount, :currency, :excluded, :notes, :entryable_type, :nature, :origin,
- entryable_attributes: [
- :id,
- :category_id,
- :merchant_id,
- { tag_ids: [] }
- ]
- ).tap do |permitted_params|
- nature = permitted_params.delete(:nature)
-
- if permitted_params[:amount]
- amount_value = permitted_params[:amount].to_d
-
- if nature == "income"
- amount_value *= -1
- end
-
- permitted_params[:amount] = amount_value
- end
- end
+ def search_params
+ params.fetch(:q, {})
+ .permit(:start_date, :end_date, :search, :amount, :amount_operator, accounts: [], account_ids: [], categories: [], merchants: [], types: [], tags: [])
end
end
diff --git a/app/controllers/account/valuations_controller.rb b/app/controllers/account/valuations_controller.rb
index 35b83b90..08f566f3 100644
--- a/app/controllers/account/valuations_controller.rb
+++ b/app/controllers/account/valuations_controller.rb
@@ -1,38 +1,3 @@
class Account::ValuationsController < ApplicationController
- layout :with_sidebar
-
- before_action :set_account
-
- def new
- @entry = @account.entries.account_valuations.new(
- currency: @account.currency,
- entryable_attributes: {}
- )
- end
-
- def create
- @entry = @account.entries.account_valuations.new(entry_params.merge(entryable_attributes: {}))
-
- if @entry.save
- @entry.sync_account_later
- redirect_back_or_to account_valuations_path(@account), notice: t(".success")
- else
- flash[:alert] = @entry.errors.full_messages.to_sentence
- redirect_to @account
- end
- end
-
- def index
- @entries = @account.entries.account_valuations.reverse_chronological
- end
-
- private
-
- def set_account
- @account = Current.family.accounts.find(params[:account_id])
- end
-
- def entry_params
- params.require(:account_entry).permit(:name, :date, :amount, :currency)
- end
+ include EntryableResource
end
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 56c29d26..8d0c27c9 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -31,6 +31,11 @@ class AccountsController < ApplicationController
redirect_to account_path(@account)
end
+ def chart
+ @account = Current.family.accounts.find(params[:id])
+ render layout: "application"
+ end
+
def sync_all
unless Current.family.syncing?
Current.family.sync_later
diff --git a/app/controllers/concerns/entryable_resource.rb b/app/controllers/concerns/entryable_resource.rb
new file mode 100644
index 00000000..84aac1d4
--- /dev/null
+++ b/app/controllers/concerns/entryable_resource.rb
@@ -0,0 +1,126 @@
+module EntryableResource
+ extend ActiveSupport::Concern
+
+ included do
+ layout :with_sidebar
+ before_action :set_entry, only: %i[show update destroy]
+ end
+
+ class_methods do
+ def permitted_entryable_attributes(*attrs)
+ @permitted_entryable_attributes = attrs if attrs.any?
+ @permitted_entryable_attributes ||= [ :id ]
+ end
+ end
+
+ def show
+ end
+
+ def new
+ account = Current.family.accounts.find_by(id: params[:account_id])
+
+ @entry = Current.family.entries.new(
+ account: account,
+ currency: account ? account.currency : Current.family.currency,
+ entryable: entryable_type.new
+ )
+ end
+
+ def create
+ @entry = build_entry
+
+ if @entry.save
+ @entry.sync_account_later
+
+ flash[:notice] = t("account.entries.create.success")
+
+ respond_to do |format|
+ format.html { redirect_back_or_to account_path(@entry.account) }
+
+ redirect_target_url = request.referer || account_path(@entry.account)
+ format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, redirect_target_url) }
+ end
+ else
+ render :new, status: :unprocessable_entity
+ end
+ end
+
+ def update
+ if @entry.update(update_entry_params)
+ @entry.sync_account_later
+
+ respond_to do |format|
+ format.html { redirect_back_or_to account_path(@entry.account), notice: t("account.entries.update.success") }
+ format.turbo_stream do
+ render turbo_stream: turbo_stream.replace(
+ "header_account_entry_#{@entry.id}",
+ partial: "#{entryable_type.name.underscore.pluralize}/header",
+ locals: { entry: @entry }
+ )
+ end
+ end
+ else
+ render :show, status: :unprocessable_entity
+ end
+ end
+
+ def destroy
+ account = @entry.account
+ @entry.destroy!
+ @entry.sync_account_later
+
+ flash[:notice] = t("account.entries.destroy.success")
+
+ respond_to do |format|
+ format.html { redirect_back_or_to account_path(account) }
+
+ redirect_target_url = request.referer || account_path(@entry.account)
+ format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, redirect_target_url) }
+ end
+ end
+
+ private
+ def entryable_type
+ permitted_entryable_types = %w[Account::Transaction Account::Valuation Account::Trade]
+ klass = params[:entryable_type] || "Account::#{controller_name.classify}"
+ klass.constantize if permitted_entryable_types.include?(klass)
+ end
+
+ def set_entry
+ @entry = Current.family.entries.find(params[:id])
+ end
+
+ def build_entry
+ Current.family.entries.new(create_entry_params)
+ end
+
+ def update_entry_params
+ prepared_entry_params
+ end
+
+ def create_entry_params
+ prepared_entry_params.merge({
+ entryable_type: entryable_type.name,
+ entryable_attributes: entry_params[:entryable_attributes] || {}
+ })
+ end
+
+ def prepared_entry_params
+ default_params = entry_params.except(:nature)
+ default_params = default_params.merge(entryable_type: entryable_type.name) if entry_params[:entryable_attributes].present?
+
+ if entry_params[:nature].present? && entry_params[:amount].present?
+ signed_amount = entry_params[:nature] == "inflow" ? -entry_params[:amount].to_d : entry_params[:amount].to_d
+ default_params = default_params.merge(amount: signed_amount)
+ end
+
+ default_params
+ end
+
+ def entry_params
+ params.require(:account_entry).permit(
+ :account_id, :name, :date, :amount, :currency, :excluded, :notes, :nature,
+ entryable_attributes: self.class.permitted_entryable_attributes
+ )
+ end
+end
diff --git a/app/controllers/securities_controller.rb b/app/controllers/securities_controller.rb
index 24356118..4a3c65c4 100644
--- a/app/controllers/securities_controller.rb
+++ b/app/controllers/securities_controller.rb
@@ -1,5 +1,18 @@
class SecuritiesController < ApplicationController
- def import
- SecuritiesImportJob.perform_later(params[:exchange_mic])
+ def index
+ query = params[:q]
+ return render json: [] if query.blank? || query.length < 2 || query.length > 100
+
+ @securities = Security.search({
+ search: query,
+ country: country_code_filter
+ })
end
+
+ private
+ def country_code_filter
+ filter = params[:country_code]
+ filter = "#{filter},US" unless filter == "US"
+ filter
+ end
end
diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb
index b1733585..acceab79 100644
--- a/app/controllers/transactions_controller.rb
+++ b/app/controllers/transactions_controller.rb
@@ -13,94 +13,13 @@ class TransactionsController < ApplicationController
}
end
- def new
- @entry = Current.family.entries.new(entryable: Account::Transaction.new).tap do |e|
- if params[:account_id]
- e.account = Current.family.accounts.find(params[:account_id])
- e.currency = e.account.currency
- else
- e.currency = Current.family.currency
- end
- end
- end
-
- def create
- @entry = Current.family
- .accounts
- .find(params[:account_entry][:account_id])
- .entries
- .create!(transaction_entry_params.merge(amount: amount))
-
- @entry.sync_account_later
- redirect_back_or_to @entry.account, notice: t(".success")
- end
-
- def bulk_delete
- destroyed = Current.family.entries.destroy_by(id: bulk_delete_params[:entry_ids])
- destroyed.map(&:account).uniq.each(&:sync_later)
- redirect_back_or_to transactions_url, notice: t(".success", count: destroyed.count)
- end
-
- def bulk_edit
- end
-
- def bulk_update
- updated = Current.family
- .entries
- .where(id: bulk_update_params[:entry_ids])
- .bulk_update!(bulk_update_params)
-
- redirect_back_or_to transactions_url, notice: t(".success", count: updated)
- end
-
- def mark_transfers
- Current.family
- .entries
- .where(id: bulk_update_params[:entry_ids])
- .mark_transfers!
-
- redirect_back_or_to transactions_url, notice: t(".success")
- end
-
- def unmark_transfers
- Current.family
- .entries
- .where(id: bulk_update_params[:entry_ids])
- .update_all marked_as_transfer: false
-
- redirect_back_or_to transactions_url, notice: t(".success")
- end
-
private
-
- def amount
- if nature.income?
- transaction_entry_params[:amount].to_d * -1
- else
- transaction_entry_params[:amount].to_d
- end
- end
-
- def nature
- params[:account_entry][:nature].to_s.inquiry
- end
-
- def bulk_delete_params
- params.require(:bulk_delete).permit(entry_ids: [])
- end
-
- def bulk_update_params
- params.require(:bulk_update).permit(:date, :notes, :category_id, :merchant_id, entry_ids: [])
- end
-
def search_params
params.fetch(:q, {})
- .permit(:start_date, :end_date, :search, :amount, :amount_operator, accounts: [], account_ids: [], categories: [], merchants: [], types: [], tags: [])
- end
-
- def transaction_entry_params
- params.require(:account_entry)
- .permit(:name, :date, :amount, :currency, :entryable_type, entryable_attributes: [ :category_id ])
- .with_defaults(entryable_type: "Account::Transaction", entryable_attributes: {})
+ .permit(
+ :start_date, :end_date, :search, :amount,
+ :amount_operator, accounts: [], account_ids: [],
+ categories: [], merchants: [], types: [], tags: []
+ )
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 3871a7d9..8bf3cf28 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -62,9 +62,9 @@ module ApplicationHelper
#
Content here
# <% end %>
#
- def drawer(&block)
+ def drawer(reload_on_close: false, &block)
content = capture &block
- render partial: "shared/drawer", locals: { content: content }
+ render partial: "shared/drawer", locals: { content:, reload_on_close: }
end
def disclosure(title, &block)
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 874eae81..12751637 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -1,3 +1,7 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails";
import "controllers";
+
+Turbo.StreamActions.redirect = function () {
+ Turbo.visit(this.target);
+};
diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js
index 4b55996d..f898dcad 100644
--- a/app/javascript/controllers/application.js
+++ b/app/javascript/controllers/application.js
@@ -6,7 +6,7 @@ const application = Application.start();
application.debug = false;
window.Stimulus = application;
-Turbo.setConfirmMethod((message) => {
+Turbo.config.forms.confirm = (message) => {
const dialog = document.getElementById("turbo-confirm");
try {
@@ -52,6 +52,6 @@ Turbo.setConfirmMethod((message) => {
{ once: true },
);
});
-});
+};
export { application };
diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js
index a988dbb8..8c9d6c50 100644
--- a/app/javascript/controllers/modal_controller.js
+++ b/app/javascript/controllers/modal_controller.js
@@ -2,6 +2,10 @@ import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="modal"
export default class extends Controller {
+ static values = {
+ reloadOnClose: { type: Boolean, default: false },
+ };
+
connect() {
if (this.element.open) return;
this.element.showModal();
@@ -10,11 +14,15 @@ export default class extends Controller {
// Hide the dialog when the user clicks outside of it
clickOutside(e) {
if (e.target === this.element) {
- this.element.close();
+ this.close();
}
}
close() {
this.element.close();
+
+ if (this.reloadOnCloseValue) {
+ window.location.reload();
+ }
}
}
diff --git a/app/javascript/controllers/trade_form_controller.js b/app/javascript/controllers/trade_form_controller.js
index cd435d55..bff84364 100644
--- a/app/javascript/controllers/trade_form_controller.js
+++ b/app/javascript/controllers/trade_form_controller.js
@@ -1,71 +1,11 @@
import { Controller } from "@hotwired/stimulus";
-const TRADE_TYPES = {
- BUY: "buy",
- SELL: "sell",
- TRANSFER_IN: "transfer_in",
- TRANSFER_OUT: "transfer_out",
- INTEREST: "interest",
-};
-
-const FIELD_VISIBILITY = {
- [TRADE_TYPES.BUY]: { ticker: true, qty: true, price: true },
- [TRADE_TYPES.SELL]: { ticker: true, qty: true, price: true },
- [TRADE_TYPES.TRANSFER_IN]: { amount: true, transferAccount: true },
- [TRADE_TYPES.TRANSFER_OUT]: { amount: true, transferAccount: true },
- [TRADE_TYPES.INTEREST]: { amount: true },
-};
-
// Connects to data-controller="trade-form"
export default class extends Controller {
- static targets = [
- "typeInput",
- "tickerInput",
- "amountInput",
- "transferAccountInput",
- "qtyInput",
- "priceInput",
- ];
-
- connect() {
- this.handleTypeChange = this.handleTypeChange.bind(this);
- this.typeInputTarget.addEventListener("change", this.handleTypeChange);
- this.updateFields(this.typeInputTarget.value || TRADE_TYPES.BUY);
- }
-
- disconnect() {
- this.typeInputTarget.removeEventListener("change", this.handleTypeChange);
- }
-
- handleTypeChange(event) {
- this.updateFields(event.target.value);
- }
-
- updateFields(type) {
- const visibleFields = FIELD_VISIBILITY[type] || {};
-
- Object.entries(this.fieldTargets).forEach(([field, target]) => {
- const isVisible = visibleFields[field] || false;
-
- // Update visibility
- target.hidden = !isVisible;
-
- // Update required status based on visibility
- if (isVisible) {
- target.setAttribute("required", "");
- } else {
- target.removeAttribute("required");
- }
- });
- }
-
- get fieldTargets() {
- return {
- ticker: this.tickerInputTarget,
- amount: this.amountInputTarget,
- transferAccount: this.transferAccountInputTarget,
- qty: this.qtyInputTarget,
- price: this.priceInputTarget,
- };
+ // Reloads the page with a new type without closing the modal
+ async changeType(event) {
+ const url = new URL(event.params.url, window.location.origin);
+ url.searchParams.set(event.params.key, event.target.value);
+ Turbo.visit(url, { frame: "modal" });
}
}
diff --git a/app/models/account.rb b/app/models/account.rb
index e9a6160f..50fa6f56 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -12,7 +12,7 @@ class Account < ApplicationRecord
has_many :transactions, through: :entries, source: :entryable, source_type: "Account::Transaction"
has_many :valuations, through: :entries, source: :entryable, source_type: "Account::Valuation"
has_many :trades, through: :entries, source: :entryable, source_type: "Account::Trade"
- has_many :holdings, dependent: :destroy
+ has_many :holdings, dependent: :destroy, class_name: "Account::Holding"
has_many :balances, dependent: :destroy
has_many :issues, as: :issuable, dependent: :destroy
diff --git a/app/models/account/entry.rb b/app/models/account/entry.rb
index 2e77b19c..2addf3be 100644
--- a/app/models/account/entry.rb
+++ b/app/models/account/entry.rb
@@ -30,10 +30,10 @@ class Account::Entry < ApplicationRecord
}
def sync_account_later
- if destroyed?
- sync_start_date = previous_entry&.date
+ sync_start_date = if destroyed?
+ previous_entry&.date
else
- sync_start_date = [ date_previously_was, date ].compact.min
+ [ date_previously_was, date ].compact.min
end
account.sync_later(start_date: sync_start_date)
diff --git a/app/models/account/entry_builder.rb b/app/models/account/entry_builder.rb
deleted file mode 100644
index 189acfdd..00000000
--- a/app/models/account/entry_builder.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-class Account::EntryBuilder
- include ActiveModel::Model
-
- TYPES = %w[income expense buy sell interest transfer_in transfer_out].freeze
-
- attr_accessor :type, :date, :qty, :ticker, :price, :amount, :currency, :account, :transfer_account_id
-
- validates :type, inclusion: { in: TYPES }
-
- def save
- if valid?
- create_builder.save
- end
- end
-
- private
-
- def create_builder
- case type
- when "buy", "sell"
- create_trade_builder
- else
- create_transaction_builder
- end
- end
-
- def create_trade_builder
- Account::TradeBuilder.new \
- type: type,
- date: date,
- qty: qty,
- ticker: ticker,
- price: price,
- account: account
- end
-
- def create_transaction_builder
- Account::TransactionBuilder.new \
- type: type,
- date: date,
- amount: amount,
- account: account,
- currency: currency,
- transfer_account_id: transfer_account_id
- end
-end
diff --git a/app/models/account/trade.rb b/app/models/account/trade.rb
index 3ab2241e..b8ebd7b8 100644
--- a/app/models/account/trade.rb
+++ b/app/models/account/trade.rb
@@ -28,8 +28,7 @@ class Account::Trade < ApplicationRecord
def name
prefix = sell? ? "Sell " : "Buy "
- generated = prefix + "#{qty.abs} shares of #{security.ticker}"
- entry.name || generated
+ prefix + "#{qty.abs} shares of #{security.ticker}"
end
def unrealized_gain_loss
diff --git a/app/models/account/trade_builder.rb b/app/models/account/trade_builder.rb
index ec252897..dd6b966c 100644
--- a/app/models/account/trade_builder.rb
+++ b/app/models/account/trade_builder.rb
@@ -1,33 +1,103 @@
-class Account::TradeBuilder < Account::EntryBuilder
+class Account::TradeBuilder
include ActiveModel::Model
- TYPES = %w[buy sell].freeze
-
- attr_accessor :type, :qty, :price, :ticker, :date, :account
-
- validates :type, :qty, :price, :ticker, :date, presence: true
- validates :price, numericality: { greater_than: 0 }
- validates :type, inclusion: { in: TYPES }
+ attr_accessor :account, :date, :amount, :currency, :qty,
+ :price, :ticker, :type, :transfer_account_id
def save
- if valid?
- create_entry
- end
+ buildable.save
+ end
+
+ def errors
+ buildable.errors
+ end
+
+ def sync_account_later
+ buildable.sync_account_later
end
private
+ def buildable
+ case type
+ when "buy", "sell"
+ build_trade
+ when "deposit", "withdrawal"
+ build_transfer
+ when "interest"
+ build_interest
+ else
+ raise "Unknown trade type: #{type}"
+ end
+ end
- def create_entry
- account.entries.account_trades.create! \
+ def build_trade
+ account.entries.new(
date: date,
- amount: amount,
- currency: account.currency,
+ amount: signed_amount,
+ currency: currency,
entryable: Account::Trade.new(
- security: security,
qty: signed_qty,
- price: price.to_d,
- currency: account.currency
+ price: price,
+ currency: currency,
+ security: security
)
+ )
+ end
+
+ def build_transfer
+ transfer_account = family.accounts.find(transfer_account_id) if transfer_account_id.present?
+
+ if transfer_account
+ from_account = type == "withdrawal" ? account : transfer_account
+ to_account = type == "withdrawal" ? transfer_account : account
+
+ Account::Transfer.build_from_accounts(
+ from_account,
+ to_account,
+ date: date,
+ amount: signed_amount
+ )
+ else
+ account.entries.build(
+ name: signed_amount < 0 ? "Deposit from #{account.name}" : "Withdrawal to #{account.name}",
+ date: date,
+ amount: signed_amount,
+ currency: currency,
+ marked_as_transfer: true,
+ entryable: Account::Transaction.new
+ )
+ end
+ end
+
+ def build_interest
+ account.entries.build(
+ name: "Interest payment",
+ date: date,
+ amount: signed_amount,
+ currency: currency,
+ entryable: Account::Transaction.new
+ )
+ end
+
+ def signed_qty
+ return nil unless type.in?([ "buy", "sell" ])
+
+ type == "sell" ? -qty.to_d : qty.to_d
+ end
+
+ def signed_amount
+ case type
+ when "buy", "sell"
+ signed_qty * price.to_d
+ when "deposit", "withdrawal"
+ type == "deposit" ? -amount.to_d : amount.to_d
+ when "interest"
+ amount.to_d * -1
+ end
+ end
+
+ def family
+ account.family
end
def security
@@ -40,14 +110,4 @@ class Account::TradeBuilder < Account::EntryBuilder
security
end
-
- def amount
- price.to_d * signed_qty
- end
-
- def signed_qty
- _qty = qty.to_d
- _qty = _qty * -1 if type == "sell"
- _qty
- end
end
diff --git a/app/models/account/transaction_builder.rb b/app/models/account/transaction_builder.rb
deleted file mode 100644
index 6c87d6a4..00000000
--- a/app/models/account/transaction_builder.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-class Account::TransactionBuilder
- include ActiveModel::Model
-
- TYPES = %w[income expense interest transfer_in transfer_out].freeze
-
- attr_accessor :type, :amount, :date, :account, :currency, :transfer_account_id
-
- validates :type, :amount, :date, presence: true
- validates :type, inclusion: { in: TYPES }
-
- def save
- if valid?
- transfer? ? create_transfer : create_transaction
- end
- end
-
- private
-
- def transfer?
- %w[transfer_in transfer_out].include?(type)
- end
-
- def create_transfer
- return create_unlinked_transfer(account.id, signed_amount) if transfer_account_id.blank?
-
- from_account_id = type == "transfer_in" ? transfer_account_id : account.id
- to_account_id = type == "transfer_in" ? account.id : transfer_account_id
-
- outflow = create_unlinked_transfer(from_account_id, signed_amount.abs)
- inflow = create_unlinked_transfer(to_account_id, signed_amount.abs * -1)
-
- Account::Transfer.create! entries: [ outflow, inflow ]
-
- inflow
- end
-
- def create_unlinked_transfer(account_id, amount)
- build_entry(account_id, amount, marked_as_transfer: true).tap(&:save!)
- end
-
- def create_transaction
- build_entry(account.id, signed_amount).tap(&:save!)
- end
-
- def build_entry(account_id, amount, marked_as_transfer: false)
- Account::Entry.new \
- account_id: account_id,
- name: marked_as_transfer ? (amount < 0 ? "Deposit" : "Withdrawal") : "Interest",
- amount: amount,
- currency: currency,
- date: date,
- marked_as_transfer: marked_as_transfer,
- entryable: Account::Transaction.new
- end
-
- def signed_amount
- case type
- when "expense", "transfer_out"
- amount.to_d
- else
- amount.to_d * -1
- end
- end
-end
diff --git a/app/models/account/transfer.rb b/app/models/account/transfer.rb
index b919c4b7..174576e8 100644
--- a/app/models/account/transfer.rb
+++ b/app/models/account/transfer.rb
@@ -48,6 +48,10 @@ class Account::Transfer < ApplicationRecord
end
end
+ def sync_account_later
+ entries.each(&:sync_account_later)
+ end
+
class << self
def build_from_accounts(from_account, to_account, date:, amount:)
outflow = from_account.entries.build \
diff --git a/app/models/concerns/accountable.rb b/app/models/concerns/accountable.rb
index 84515374..6c93a8f8 100644
--- a/app/models/concerns/accountable.rb
+++ b/app/models/concerns/accountable.rb
@@ -35,8 +35,9 @@ module Accountable
end
def post_sync
- broadcast_remove_to(account, target: "syncing-notification")
+ broadcast_remove_to(account.family, target: "syncing-notice")
+ # Broadcast a simple replace event that the controller can handle
broadcast_replace_to(
account,
target: "chart_account_#{account.id}",
diff --git a/app/models/family.rb b/app/models/family.rb
index a618220c..e32c0d78 100644
--- a/app/models/family.rb
+++ b/app/models/family.rb
@@ -15,6 +15,7 @@ class Family < ApplicationRecord
has_many :categories, dependent: :destroy
has_many :merchants, dependent: :destroy
has_many :issues, through: :accounts
+ has_many :holdings, through: :accounts
has_many :plaid_items, dependent: :destroy
validates :locale, inclusion: { in: I18n.available_locales.map(&:to_s) }
diff --git a/app/models/investment.rb b/app/models/investment.rb
index ced62765..90519da3 100644
--- a/app/models/investment.rb
+++ b/app/models/investment.rb
@@ -56,7 +56,7 @@ class Investment < ApplicationRecord
end
def post_sync
- broadcast_remove_to(account, target: "syncing-notification")
+ broadcast_remove_to(account, target: "syncing-notice")
broadcast_replace_to(
account,
diff --git a/app/models/provider/synth.rb b/app/models/provider/synth.rb
index 8247ab34..5044f00a 100644
--- a/app/models/provider/synth.rb
+++ b/app/models/provider/synth.rb
@@ -134,12 +134,12 @@ class Provider::Synth
securities = parsed.dig("data").map do |security|
{
- symbol: security.dig("symbol"),
+ ticker: security.dig("symbol"),
name: security.dig("name"),
logo_url: security.dig("logo_url"),
exchange_acronym: security.dig("exchange", "acronym"),
exchange_mic: security.dig("exchange", "mic_code"),
- exchange_country_code: security.dig("exchange", "country_code")
+ country_code: security.dig("exchange", "country_code")
}
end
diff --git a/app/models/security.rb b/app/models/security.rb
index 732599ce..d2ce6387 100644
--- a/app/models/security.rb
+++ b/app/models/security.rb
@@ -8,17 +8,33 @@ class Security < ApplicationRecord
validates :ticker, presence: true
validates :ticker, uniqueness: { scope: :exchange_mic, case_sensitive: false }
+ class << self
+ def search(query)
+ security_prices_provider.search_securities(
+ query: query[:search],
+ dataset: "limited",
+ country_code: query[:country]
+ ).securities.map { |attrs| new(**attrs) }
+ end
+ end
+
def current_price
@current_price ||= Security::Price.find_price(security: self, date: Date.current)
return nil if @current_price.nil?
Money.new(@current_price.price, @current_price.currency)
end
- def to_combobox_display
- "#{ticker} (#{exchange_acronym})"
+ def to_combobox_option
+ SynthComboboxOption.new(
+ symbol: ticker,
+ name: name,
+ logo_url: logo_url,
+ exchange_acronym: exchange_acronym,
+ exchange_mic: exchange_mic,
+ exchange_country_code: country_code
+ )
end
-
private
def upcase_ticker
diff --git a/app/models/security/synth_combobox_option.rb b/app/models/security/synth_combobox_option.rb
index efd81db6..d3b4437d 100644
--- a/app/models/security/synth_combobox_option.rb
+++ b/app/models/security/synth_combobox_option.rb
@@ -1,22 +1,8 @@
class Security::SynthComboboxOption
include ActiveModel::Model
- include Providable
attr_accessor :symbol, :name, :logo_url, :exchange_acronym, :exchange_mic, :exchange_country_code
- class << self
- def find_in_synth(query)
- country = Current.family.country
- country = "#{country},US" unless country == "US"
-
- security_prices_provider.search_securities(
- query:,
- dataset: "limited",
- country_code: country
- ).securities.map { |attrs| new(**attrs) }
- end
- end
-
def id
"#{symbol}|#{exchange_mic}|#{exchange_acronym}|#{exchange_country_code}" # submitted by combobox as value
end
diff --git a/app/views/account/entries/_entry.html.erb b/app/views/account/entries/_entry.html.erb
index 4b839916..9bfe063a 100644
--- a/app/views/account/entries/_entry.html.erb
+++ b/app/views/account/entries/_entry.html.erb
@@ -1,5 +1,5 @@
-<%# locals: (entry:, selectable: true, show_balance: false, origin: nil) %>
+<%# locals: (entry:, selectable: true, show_balance: false) %>
<%= turbo_frame_tag dom_id(entry) do %>
- <%= render partial: entry.entryable.to_partial_path, locals: { entry:, selectable:, show_balance:, origin: } %>
+ <%= render partial: entry.entryable.to_partial_path, locals: { entry:, selectable:, show_balance: } %>
<% end %>
diff --git a/app/views/account/entries/_selection_bar.html.erb b/app/views/account/entries/_selection_bar.html.erb
index f4f7208e..3bbf39d1 100644
--- a/app/views/account/entries/_selection_bar.html.erb
+++ b/app/views/account/entries/_selection_bar.html.erb
@@ -6,7 +6,7 @@
- <%= form_with url: bulk_delete_transactions_path, data: { turbo_confirm: true, turbo_frame: "_top" } do %>
+ <%= form_with url: bulk_delete_account_transactions_path, data: { turbo_confirm: true, turbo_frame: "_top" } do %>
diff --git a/app/views/account/entries/index.html.erb b/app/views/account/entries/index.html.erb
index 9e6d5d42..c66d3767 100644
--- a/app/views/account/entries/index.html.erb
+++ b/app/views/account/entries/index.html.erb
@@ -9,13 +9,13 @@
<%= tag.span t(".new") %>
- <%= link_to new_account_valuation_path(@account), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
+ <%= link_to new_account_valuation_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
<%= lucide_icon("circle-dollar-sign", class: "text-gray-500 w-5 h-5") %>
<%= tag.span t(".new_balance"), class: "text-sm" %>
<% end %>
<% unless @account.crypto? %>
- <%= link_to @account.investment? ? new_account_trade_path(@account) : new_transaction_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
+ <%= link_to @account.investment? ? new_account_trade_path(account_id: @account.id) : new_account_transaction_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
<%= lucide_icon("credit-card", class: "text-gray-500 w-5 h-5") %>
<%= tag.span t(".new_transaction"), class: "text-sm" %>
<% end %>
@@ -75,7 +75,7 @@
<%= entries_by_date(@entries) do |entries| %>
- <%= render entries, show_balance: true, origin: "account" %>
+ <%= render entries, show_balance: true %>
<% end %>
diff --git a/app/views/account/holdings/_holding.html.erb b/app/views/account/holdings/_holding.html.erb
index d8af9e8c..c70e6515 100644
--- a/app/views/account/holdings/_holding.html.erb
+++ b/app/views/account/holdings/_holding.html.erb
@@ -6,7 +6,7 @@
<%= image_tag "https://logo.synthfinance.com/ticker/#{holding.ticker}", class: "w-9 h-9 rounded-full" %>
- <%= link_to holding.name, account_holding_path(holding.account, holding), data: { turbo_frame: :drawer }, class: "hover:underline" %>
+ <%= link_to holding.name, account_holding_path(holding), data: { turbo_frame: :drawer }, class: "hover:underline" %>
<% if holding.amount %>
<%= tag.p holding.ticker, class: "text-gray-500 text-xs uppercase" %>
diff --git a/app/views/account/holdings/show.html.erb b/app/views/account/holdings/show.html.erb
index 6af0e451..92475cf1 100644
--- a/app/views/account/holdings/show.html.erb
+++ b/app/views/account/holdings/show.html.erb
@@ -101,10 +101,10 @@
<%= button_to t(".delete"),
- account_holding_path(@holding.account, @holding),
+ account_holding_path(@holding),
method: :delete,
class: "rounded-lg px-3 py-2 text-red-500 text-sm font-medium border border-alpha-black-200",
- data: { turbo_confirm: true, turbo_frame: "_top" } %>
+ data: { turbo_confirm: true } %>
diff --git a/app/views/account/trades/_form.html.erb b/app/views/account/trades/_form.html.erb
index a3330927..fbb288df 100644
--- a/app/views/account/trades/_form.html.erb
+++ b/app/views/account/trades/_form.html.erb
@@ -1,35 +1,51 @@
<%# locals: (entry:) %>
-<%= styled_form_with data: { turbo_frame: "_top", controller: "trade-form" },
- model: entry,
- scope: :account_entry,
- url: account_trades_path(entry.account) do |form| %>
+<% type = params[:type] || "buy" %>
+
+<%= styled_form_with model: entry, url: account_trades_path, data: { controller: "trade-form" } do |form| %>
+
+ <%= form.hidden_field :account_id %>
+
+ <% if entry.errors.any? %>
+ <%= render "shared/form_errors", model: entry %>
+ <% end %>
+
- <%= form.select :type, options_for_select([%w[Buy buy], %w[Sell sell], %w[Deposit transfer_in], %w[Withdrawal transfer_out], %w[Interest interest]], "buy"), { label: t(".type") }, { data: { "trade-form-target": "typeInput" } } %>
-
+ <%= form.select :type, [
+ ["Buy", "buy"],
+ ["Sell", "sell"],
+ ["Deposit", "deposit"],
+ ["Withdrawal", "withdrawal"],
+ ["Interest", "interest"]
+ ],
+ { label: t(".type"), selected: type },
+ { data: {
+ action: "trade-form#changeType",
+ trade_form_url_param: new_account_trade_path(account_id: entry.account_id),
+ trade_form_key_param: "type",
+ }} %>
+
+ <% if %w[buy sell].include?(type) %>
- <%= form.combobox :ticker, securities_account_trades_path(entry.account), label: t(".holding"), placeholder: t(".ticker_placeholder") %>
+ <%= form.combobox :ticker, securities_path(country_code: Current.family.country), label: t(".holding"), placeholder: t(".ticker_placeholder"), required: true %>
-
+ <% end %>
- <%= form.date_field :date, label: true, value: Date.today %>
+ <%= form.date_field :date, label: true, value: Date.today, required: true %>
-
- <%= form.money_field :amount, label: t(".amount") %>
-
+ <% unless %w[buy sell].include?(type) %>
+ <%= form.money_field :amount, label: t(".amount"), required: true %>
+ <% end %>
-
+ <% if %w[deposit withdrawal].include?(type) %>
<%= form.collection_select :transfer_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") } %>
-
+ <% end %>
-
- <%= form.number_field :qty, label: t(".qty"), placeholder: "10", min: 0.000000000000000001, step: "any" %>
-
-
-
- <%= form.money_field :price, label: t(".price"), currency_value_override: "USD", disable_currency: true %>
-
+ <% if %w[buy sell].include?(type) %>
+ <%= form.number_field :qty, label: t(".qty"), placeholder: "10", min: 0.000000000000000001, step: "any", required: true %>
+ <%= form.money_field :price, label: t(".price"), required: true %>
+ <% end %>
<%= form.submit t(".submit") %>
diff --git a/app/views/account/trades/_header.html.erb b/app/views/account/trades/_header.html.erb
new file mode 100644
index 00000000..7ccadfa4
--- /dev/null
+++ b/app/views/account/trades/_header.html.erb
@@ -0,0 +1,68 @@
+<%# locals: (entry:) %>
+
+
diff --git a/app/views/account/trades/_security.turbo_stream.erb b/app/views/account/trades/_security.turbo_stream.erb
deleted file mode 100644
index 34bdebd3..00000000
--- a/app/views/account/trades/_security.turbo_stream.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-
- <%= image_tag(security.logo_url, class: "rounded-full h-8 w-8 inline-block mr-2" ) %>
-
-
- <%= security.name.presence || security.symbol %>
-
-
- <%= "#{security.symbol} (#{security.exchange_acronym})" %>
-
-
-
diff --git a/app/views/account/trades/_selection_bar.html.erb b/app/views/account/trades/_selection_bar.html.erb
index f4f7208e..3bbf39d1 100644
--- a/app/views/account/trades/_selection_bar.html.erb
+++ b/app/views/account/trades/_selection_bar.html.erb
@@ -6,7 +6,7 @@
- <%= form_with url: bulk_delete_transactions_path, data: { turbo_confirm: true, turbo_frame: "_top" } do %>
+ <%= form_with url: bulk_delete_account_transactions_path, data: { turbo_confirm: true, turbo_frame: "_top" } do %>
diff --git a/app/views/account/trades/_trade.html.erb b/app/views/account/trades/_trade.html.erb
index 29cd40f4..bd91d147 100644
--- a/app/views/account/trades/_trade.html.erb
+++ b/app/views/account/trades/_trade.html.erb
@@ -1,8 +1,8 @@
-<%# locals: (entry:, selectable: true, show_balance: false, origin: nil) %>
+<%# locals: (entry:, selectable: true, show_balance: false) %>
<% trade, account = entry.account_trade, entry.account %>
-
+
text-sm font-medium p-4">
<% if selectable %>
<%= check_box_tag dom_id(entry, "selection"),
@@ -16,12 +16,12 @@
<%= trade.name.first.upcase %>
-
+
<% if entry.new_record? %>
<%= content_tag :p, trade.name %>
<% else %>
<%= link_to trade.name,
- account_entry_path(account, entry),
+ account_entry_path(entry),
data: { turbo_frame: "drawer", turbo_prefetch: false },
class: "hover:underline hover:text-gray-800" %>
<% end %>
@@ -31,7 +31,9 @@
- <%= tag.span format_money(entry.amount_money) %>
+ <%= content_tag :p,
+ format_money(-entry.amount_money),
+ class: ["text-green-600": entry.amount.negative?] %>
diff --git a/app/views/account/trades/securities.turbo_stream.erb b/app/views/account/trades/securities.turbo_stream.erb
deleted file mode 100644
index a3225939..00000000
--- a/app/views/account/trades/securities.turbo_stream.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<%= async_combobox_options @securities,
- render_in: { partial: "account/trades/security" } %>
diff --git a/app/views/account/trades/show.html.erb b/app/views/account/trades/show.html.erb
index 0f07e9ad..1a1d8cf6 100644
--- a/app/views/account/trades/show.html.erb
+++ b/app/views/account/trades/show.html.erb
@@ -1,83 +1,37 @@
-<% entry, trade, account = @entry, @entry.account_trade, @entry.account %>
+<%= drawer(reload_on_close: true) do %>
+ <%= render "account/trades/header", entry: @entry %>
-<%= drawer do %>
-
+ <% trade = @entry.account_trade %>
-
- <%= disclosure t(".overview") do %>
-
-
-
-
- <%= t(".symbol_label") %>
- - <%= trade.security.ticker %>
-
-
- <% if trade.buy? %>
-
-
- <%= t(".purchase_qty_label") %>
- - <%= trade.qty.abs %>
-
-
-
-
- <%= t(".purchase_price_label") %>
- - <%= format_money trade.price_money %>
-
- <% end %>
-
- <% if trade.security.current_price.present? %>
-
-
- <%= t(".current_market_price_label") %>
- - <%= format_money trade.security.current_price %>
-
- <% end %>
-
- <% if trade.buy? && trade.unrealized_gain_loss.present? %>
-
-
- <%= t(".total_return_label") %>
- -
- <%= render "shared/trend_change", trend: trade.unrealized_gain_loss %>
-
-
- <% end %>
-
-
- <% end %>
-
<%= disclosure t(".details") do %>
- <%= styled_form_with model: [account, entry],
- url: account_trade_path(account, entry),
+ <%= styled_form_with model: @entry,
+ url: account_trade_path(@entry),
class: "space-y-2",
data: { controller: "auto-submit-form" } do |f| %>
<%= f.date_field :date,
label: t(".date_label"),
- max: Date.current,
+ max: Date.today,
"data-auto-submit-form-target": "auto" %>
- <%= f.fields_for :entryable do |ef| %>
- <%= ef.number_field :qty,
+
+ <%= f.select :nature,
+ [["Buy", "outflow"], ["Sell", "inflow"]],
+ { container_class: "w-1/3", label: "Type", selected: @entry.amount.negative? ? "inflow" : "outflow" },
+ { data: { "auto-submit-form-target": "auto" } } %>
+
+ <%= f.fields_for :entryable do |ef| %>
+ <%= ef.number_field :qty,
label: t(".quantity_label"),
step: "any",
+ value: trade.qty.abs,
"data-auto-submit-form-target": "auto" %>
+ <% end %>
+
+ <%= f.fields_for :entryable do |ef| %>
<%= ef.money_field :price,
label: t(".cost_per_share_label"),
disable_currency: true,
@@ -91,8 +45,8 @@
<%= disclosure t(".additional") do %>
- <%= styled_form_with model: [account, entry],
- url: account_trade_path(account, entry),
+ <%= styled_form_with model: @entry,
+ url: account_trade_path(@entry),
class: "space-y-2",
data: { controller: "auto-submit-form" } do |f| %>
<%= f.text_area :notes,
@@ -108,8 +62,8 @@
<%= disclosure t(".settings") do %>
- <%= styled_form_with model: [account, entry],
- url: account_trade_path(account, entry),
+ <%= styled_form_with model: @entry,
+ url: account_trade_path(@entry),
class: "p-3",
data: { controller: "auto-submit-form" } do |f| %>
@@ -136,11 +90,11 @@
<%= button_to t(".delete"),
- account_entry_path(account, entry),
+ account_entry_path(@entry),
method: :delete,
class: "rounded-lg px-3 py-2 text-red-500 text-sm
font-medium border border-alpha-black-200",
- data: { turbo_confirm: true, turbo_frame: "_top" } %>
+ data: { turbo_confirm: true } %>
<% end %>
diff --git a/app/views/transactions/_form.html.erb b/app/views/account/transactions/_form.html.erb
similarity index 65%
rename from app/views/transactions/_form.html.erb
rename to app/views/account/transactions/_form.html.erb
index dad6157b..10c09d30 100644
--- a/app/views/transactions/_form.html.erb
+++ b/app/views/account/transactions/_form.html.erb
@@ -1,8 +1,13 @@
-<%= styled_form_with model: @entry, url: transactions_path, class: "space-y-4", data: { turbo_frame: "_top" } do |f| %>
+<%= styled_form_with model: @entry, url: account_transactions_path, class: "space-y-4" do |f| %>
+
+ <% if entry.errors.any? %>
+ <%= render "shared/form_errors", model: entry %>
+ <% end %>
+