diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 33b3980a..f003ab31 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -31,7 +31,7 @@ class AccountsController < ApplicationController family.sync_later end - redirect_to accounts_path + redirect_back_or_to accounts_path end private diff --git a/app/controllers/rules_controller.rb b/app/controllers/rules_controller.rb index ed1f1f46..a5661508 100644 --- a/app/controllers/rules_controller.rb +++ b/app/controllers/rules_controller.rb @@ -38,7 +38,7 @@ class RulesController < ApplicationController def apply @rule.update!(active: true) - @rule.apply_later + @rule.apply_later(ignore_attribute_locks: true) redirect_back_or_to rules_path, notice: "#{@rule.resource_type.humanize} rule activated" end diff --git a/app/controllers/transaction_categories_controller.rb b/app/controllers/transaction_categories_controller.rb index b02ca194..dac6b5be 100644 --- a/app/controllers/transaction_categories_controller.rb +++ b/app/controllers/transaction_categories_controller.rb @@ -15,14 +15,17 @@ class TransactionCategoriesController < ApplicationController } end + transaction.lock_saved_attributes! + @entry.lock_saved_attributes! + respond_to do |format| format.html { redirect_back_or_to transaction_path(@entry) } format.turbo_stream do render turbo_stream: [ turbo_stream.replace( - dom_id(@entry, :category_menu), + dom_id(transaction, :category_menu), partial: "categories/menu", - locals: { transaction: @entry.transaction } + locals: { transaction: transaction } ), *flash_notification_stream_items ] diff --git a/app/javascript/controllers/rule/conditions_controller.js b/app/javascript/controllers/rule/conditions_controller.js index a3ebdce9..63dcd17a 100644 --- a/app/javascript/controllers/rule/conditions_controller.js +++ b/app/javascript/controllers/rule/conditions_controller.js @@ -54,8 +54,8 @@ export default class extends Controller { for (const operator of conditionFilter.operators) { const optionEl = document.createElement("option"); - optionEl.value = operator; - optionEl.textContent = operator; + optionEl.value = operator[1]; + optionEl.textContent = operator[0]; this.operatorSelectTarget.appendChild(optionEl); } } diff --git a/app/jobs/rule_job.rb b/app/jobs/rule_job.rb index 8b17d140..d7bbcd2d 100644 --- a/app/jobs/rule_job.rb +++ b/app/jobs/rule_job.rb @@ -1,7 +1,7 @@ class RuleJob < ApplicationJob queue_as :default - def perform(rule) - rule.apply + def perform(rule, ignore_attribute_locks: false) + rule.apply(ignore_attribute_locks: ignore_attribute_locks) end end diff --git a/app/models/rule.rb b/app/models/rule.rb index a02fab23..812dbff0 100644 --- a/app/models/rule.rb +++ b/app/models/rule.rb @@ -40,14 +40,14 @@ class Rule < ApplicationRecord matching_resources_scope.count end - def apply + def apply(ignore_attribute_locks: false) actions.each do |action| - action.apply(matching_resources_scope) + action.apply(matching_resources_scope, ignore_attribute_locks: ignore_attribute_locks) end end - def apply_later - RuleJob.perform_later(self) + def apply_later(ignore_attribute_locks: false) + RuleJob.perform_later(self, ignore_attribute_locks: ignore_attribute_locks) end private diff --git a/app/models/rule/condition_filter.rb b/app/models/rule/condition_filter.rb index 48655982..492dbc8f 100644 --- a/app/models/rule/condition_filter.rb +++ b/app/models/rule/condition_filter.rb @@ -4,9 +4,9 @@ class Rule::ConditionFilter TYPES = [ "text", "number", "select" ] OPERATORS_MAP = { - "text" => [ "like", "=" ], - "number" => [ ">", ">=", "<", "<=", "=" ], - "select" => [ "=" ] + "text" => [ [ "Contains", "like" ], [ "Equal to", "=" ] ], + "number" => [ [ "Greater than", ">" ], [ "Greater or equal to", ">=" ], [ "Less than", "<" ], [ "Less than or equal to", "<=" ], [ "Is equal to", "=" ] ], + "select" => [ [ "Equal to", "=" ] ] } def initialize(rule) @@ -70,7 +70,7 @@ class Rule::ConditionFilter end def sanitize_operator(operator) - raise UnsupportedOperatorError, "Unsupported operator: #{operator} for type: #{type}" unless operators.include?(operator) + raise UnsupportedOperatorError, "Unsupported operator: #{operator} for type: #{type}" unless operators.map(&:last).include?(operator) if operator == "like" "ILIKE" diff --git a/app/views/rule/actions/_action.html.erb b/app/views/rule/actions/_action.html.erb index 0af732c3..22d3fbbe 100644 --- a/app/views/rule/actions/_action.html.erb +++ b/app/views/rule/actions/_action.html.erb @@ -8,9 +8,11 @@ <%= form.hidden_field :_destroy, value: false, data: { rule__actions_target: "destroyField" } %>
- <%= form.select :action_type, rule.action_executors.map { |executor| [ executor.label, executor.key ] }, {}, data: { action: "rule--actions#handleActionTypeChange" } %> +
+ <%= form.select :action_type, rule.action_executors.map { |executor| [ executor.label, executor.key ] }, {}, data: { action: "rule--actions#handleActionTypeChange" } %> +
- <%= tag.div class: class_names("flex items-center gap-2", "hidden" => !needs_value), + <%= tag.div class: class_names("min-w-1/2 flex items-center gap-2", "hidden" => !needs_value), data: { rule__actions_target: "actionValue" } do %> to <%= form.select :value, action.options, {}, disabled: !needs_value %> diff --git a/app/views/rule/conditions/_condition.html.erb b/app/views/rule/conditions/_condition.html.erb index ad1b0b9d..9bae63c1 100644 --- a/app/views/rule/conditions/_condition.html.erb +++ b/app/views/rule/conditions/_condition.html.erb @@ -13,15 +13,21 @@
<%= form.hidden_field :_destroy, value: false, data: { rule__conditions_target: "destroyField" } %> - <%= form.select :condition_type, rule.condition_filters.map { |filter| [ filter.label, filter.key ] }, {}, data: { action: "rule--conditions#handleConditionTypeChange" } %> +
+ <%= form.select :condition_type, rule.condition_filters.map { |filter| [ filter.label, filter.key ] }, {}, data: { action: "rule--conditions#handleConditionTypeChange" } %> +
- <%= form.select :operator, condition.operators, { container_class: "w-fit min-w-16" }, data: { rule__conditions_target: "operatorSelect" } %> + <%= form.select :operator, condition.operators, { container_class: "w-fit min-w-36" }, data: { rule__conditions_target: "operatorSelect" } %> -
+
<% if condition.filter.type == "select" %> <%= form.select :value, condition.options, {} %> <% else %> - <%= form.text_field :value, placeholder: "Enter a value" %> + <% if condition.filter.type == "number" %> + <%= form.number_field :value, placeholder: "10" %> + <% else %> + <%= form.text_field :value, placeholder: "Enter a value" %> + <% end %> <% end %>
diff --git a/app/views/rules/_form.html.erb b/app/views/rules/_form.html.erb index c0b7920c..ce0aaf3a 100644 --- a/app/views/rules/_form.html.erb +++ b/app/views/rules/_form.html.erb @@ -1,6 +1,6 @@ <%# locals: (rule:) %> -<%= styled_form_with model: rule, class: "space-y-4", +<%= styled_form_with model: rule, class: "space-y-4 w-[550px]", data: { controller: "rules", rule_registry_value: rule.registry.to_json } do |f| %> <%= f.hidden_field :resource_type, value: rule.resource_type %> @@ -80,13 +80,13 @@
<%= f.radio_button :effective_date_enabled, false, checked: rule.effective_date.nil?, data: { action: "rules#clearEffectiveDate" } %> - <%= f.label :effective_date_enabled_false, "To all past and future #{rule.resource_type}s" %> + <%= f.label :effective_date_enabled_false, "To all past and future #{rule.resource_type}s", class: "text-sm" %>
<%= f.radio_button :effective_date_enabled, true, checked: rule.effective_date.present? %> - <%= f.label :effective_date_enabled_true, "Starting from" %> + <%= f.label :effective_date_enabled_true, "Starting from", class: "text-sm" %>
<%= f.date_field :effective_date, container_class: "w-fit", data: { rules_target: "effectiveDateInput" } %> diff --git a/app/views/rules/_rule.html.erb b/app/views/rules/_rule.html.erb index 7a4b0889..91f4bd92 100644 --- a/app/views/rules/_rule.html.erb +++ b/app/views/rules/_rule.html.erb @@ -2,7 +2,6 @@
-

<%= rule.actions.first.executor.label %> diff --git a/app/views/transactions/index.html.erb b/app/views/transactions/index.html.erb index e563aaf9..14d5fa6b 100644 --- a/app/views/transactions/index.html.erb +++ b/app/views/transactions/index.html.erb @@ -4,6 +4,11 @@

<%= contextual_menu do %> + <% if Rails.env.development? %> + <%= button_to "Dev only: Sync all", sync_all_accounts_path, class: "btn btn--ghost w-full" %> + <% end %> + <%= contextual_menu_item "New rule", url: new_rule_path(resource_type: "transaction"), icon: "plus", turbo_frame: :modal %> + <%= contextual_menu_item "Edit rules", url: rules_path, icon: "git-branch", turbo_frame: :_top %> <%= contextual_menu_modal_action_item t(".edit_categories"), categories_path, icon: "shapes", turbo_frame: :_top %> <%= contextual_menu_modal_action_item t(".edit_tags"), tags_path, icon: "tags", turbo_frame: :_top %> <%= contextual_menu_modal_action_item t(".edit_merchants"), family_merchants_path, icon: "store", turbo_frame: :_top %> diff --git a/db/migrate/20250411191422_create_data_enrichments.rb b/db/migrate/20250415125256_data_enrichments_and_locks.rb similarity index 75% rename from db/migrate/20250411191422_create_data_enrichments.rb rename to db/migrate/20250415125256_data_enrichments_and_locks.rb index a7a0fe26..7ae9a185 100644 --- a/db/migrate/20250411191422_create_data_enrichments.rb +++ b/db/migrate/20250415125256_data_enrichments_and_locks.rb @@ -1,4 +1,4 @@ -class CreateDataEnrichments < ActiveRecord::Migration[7.2] +class DataEnrichmentsAndLocks < ActiveRecord::Migration[7.2] def change create_table :data_enrichments, id: :uuid do |t| t.references :enrichable, polymorphic: true, null: false, type: :uuid @@ -12,10 +12,10 @@ class CreateDataEnrichments < ActiveRecord::Migration[7.2] add_index :data_enrichments, [ :enrichable_id, :enrichable_type, :source, :attribute_name ], unique: true # Entries - add_column :account_entries, :locked_attributes, :jsonb, default: {} - add_column :account_transactions, :locked_attributes, :jsonb, default: {} - add_column :account_trades, :locked_attributes, :jsonb, default: {} - add_column :account_valuations, :locked_attributes, :jsonb, default: {} + add_column :entries, :locked_attributes, :jsonb, default: {} + add_column :transactions, :locked_attributes, :jsonb, default: {} + add_column :trades, :locked_attributes, :jsonb, default: {} + add_column :valuations, :locked_attributes, :jsonb, default: {} # Accounts add_column :accounts, :locked_attributes, :jsonb, default: {} diff --git a/db/schema.rb b/db/schema.rb index 899125d7..a34d7b59 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_04_13_141446) do +ActiveRecord::Schema[7.2].define(version: 2025_04_15_125256) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql"